diff --git a/CHANGELOG b/CHANGELOG
index 21d979d9f..8e5d20a93 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -26,8 +26,8 @@ API Changes
the command line keep their values. Every game in a library therefore
behaves the same whether it was started from the menu or from a shell.
-- A game can be described rather than written. Singe/Author.singe and
- Singe/AuthorCompile.singe take a table of layers, entities, behaviours
+- A game can be described rather than written. Forge/Author.singe and
+ Forge/AuthorCompile.singe take a table of layers, entities, behaviours
and rules and compile it into an ordinary Singe game: the rules become
real Lua, so nothing walks a table every frame and the result can be
read and edited by hand. There is no notion of genre in it -- a game
@@ -38,6 +38,19 @@ API Changes
releases, and the "lua" action is the deliberate way out when a rule
needs something the vocabulary cannot say. See the manual.
+- Forge runs as a game rather than only as a library: started from the
+ menu it opens on a chooser (its own descriptions, a new game from a
+ starter, a copy of anything dropped into its directory), the keys, the
+ mouse and the pad reach the editor, P saves, builds and plays what is on
+ screen and comes back to it, and ESC closes or leaves. Entities can be
+ added, duplicated, deleted and typed field by field -- position, look,
+ behaviours and their parameters, all from the manifest -- and renaming
+ one renames it in every rule. Conditions and actions are picked from
+ the vocabulary with their help beside them, rules reorder, a rule's
+ note is editable, and U undoes forty steps. The compiler loads the
+ runtime from Forge/, where it lives, so it no longer fails looking for
+ a Singe/Author.singe that never shipped.
+
- An editor for those descriptions, Forge, which is itself
a Singe game: the canvas is the same overlay at the same coordinates the
game will be played in, so what is placed is what is seen. Entity list
@@ -1461,6 +1474,59 @@ Fixes
materialNew, sceneGetSize and sceneGetStats no longer reject extra
arguments.
+- A review of the week's work (the io patch, the scheduler, Forge, the
+ menu, the GLES backend, saves and the GUI) fixed what it found:
+
+ Lua's io on a packed asset: f:lines and io.lines with more than one
+ format handed back only the last value read a step; a later format
+ that comes up empty now returns what came before it with nil in its
+ place, as Lua does, and only an empty first read ends the loop.
+ read("a") reads in blocks rather than a byte at a time, read(0) is the
+ end-of-file test it is in Lua, and a NUL byte is no longer taken for
+ part of a number. testScripts/packedIo covers all of it.
+
+ Timer and tween handles carry a generation: a handle kept past its
+ timer's end no longer cancels, or answers for, whatever timer was
+ given the same slot afterwards.
+
+ Forge: a rule's keyHeld condition never fired from a real keyboard
+ (it compared a key name against the scancodes the engine delivers);
+ an entity whose look kind is unknown is drawn as a box with a message
+ instead of ending the game; and the compiler loads the runtime from
+ Forge/, where it lives, rather than a Singe/Author.singe that was never
+ shipped. The games.dat Forge writes no longer carries a DATA key the
+ engine does not read, and the manual's example loses it too.
+
+ The menu: a games.dat entry with no ATTRACT clip no longer stops the
+ engine on either renderer, quitting with no games listed no longer
+ fails in onShutdown, and a directory entry lfs cannot describe is
+ skipped rather than indexed.
+
+ The GLES backend freed nothing when its framebuffer cache filled and
+ was reset; the framebuffer objects go now. A save is written with a
+ single atomic rename everywhere but Windows, so the old save stays
+ whole until the new one is in place. A GUI texture decoded with a
+ padded pitch is packed row by row instead of uploaded with its padding
+ read as pixels. The menu's backdrop stops rewriting its grid colours
+ every frame once the grid is lit.
+
+ Net.singe verified a connection against Linux's CA list by path, so
+ without a pin every HTTPS request failed on Windows, macOS and the
+ handhelds. The engine now extracts Mozilla's CA list as
+ Singe/cacerts.pem with the other support files and Net.singe uses it
+ wherever the platform's own list is missing. A chunked response --
+ what an HTTP/1.1 server sends when it does not know the length -- is
+ unwrapped as it arrives rather than handed to the caller with the chunk
+ sizes still in it. And a mistyped value in a table given to scriptPush
+ or scriptExecute is reported like any other bad API argument -- the
+ script line and the call named -- rather than as a bare exit message.
+
+- Forge has its own manual, docs/Forge.adoc, built to Forge.html and
+ Forge.pdf beside the Singe manual; the engine's manual keeps a pointer.
+ The build packs Forge.game itself, with the manual inside, and Forge's
+ first run copies Forge.pdf out to its data directory and says where.
+ R redoes what U undid, forty steps deep.
+
SINGE 2.10
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1ecb6114a..2a6e8285e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -209,6 +209,7 @@ singeEmbed(${CMAKE_SOURCE_DIR}/assets/Tools.singe ${GENERATED_DIR}/Tools_singe.h
# its own, not with the engine. Nothing of it -- not the editor, not the compiler, not the runtime
# a game it builds loads -- belongs inside Singe.
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Net.singe ${GENERATED_DIR}/Net_singe.h "")
+singeEmbed(${CMAKE_SOURCE_DIR}/assets/cacerts.pem ${GENERATED_DIR}/cacerts_pem.h "")
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Master.singe ${GENERATED_DIR}/Master_singe.h "")
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Backdrop.singe ${GENERATED_DIR}/Backdrop_singe.h "")
singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuDocument.singe ${GENERATED_DIR}/MenuDocument_singe.h "")
@@ -295,6 +296,18 @@ singeEmbedLua(thirdparty/lume/lume.lua "")
singeEmbedLua(thirdparty/inspect.lua/inspect.lua "")
singeEmbedLua(thirdparty/bump.lua/bump.lua "")
+# Forge's manual, rendered the same way. It is not embedded: the superbuild packs it into
+# Forge.game (cmake/Superbuild.cmake), which is the one thing that ships Forge.
+add_custom_command(
+ OUTPUT ${MANUAL_DIR}/Forge.pdf
+ COMMAND ${CMAKE_COMMAND} -E make_directory ${MANUAL_DIR}
+ COMMAND ${ASCIIDOCTOR_PDF} -a revnumber=${PROJECT_VERSION} ${CMAKE_SOURCE_DIR}/docs/Forge.adoc -o ${MANUAL_DIR}/Forge.pdf
+ DEPENDS docs/Forge.adoc
+ COMMENT "Rendering Forge.pdf"
+ VERBATIM
+)
+add_custom_target(forgeManual ALL DEPENDS ${MANUAL_DIR}/Forge.pdf)
+
# Optional HTML manual for browsing: cmake --build . --target docs
if(ASCIIDOCTOR)
add_custom_target(docs
@@ -819,7 +832,6 @@ target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_BINARY_DIR}/gen
# Rockchip's hardware decoders are reached by name (videoPlayer.c), and only exist when FFmpeg was
# built against the Rockchip MPP library. Asking FFmpeg itself is what decides it.
-execute_process(COMMAND ${CMAKE_COMMAND} -E echo "" OUTPUT_QUIET)
if(EXISTS ${BUILD_DIR}/lib/pkgconfig/libavcodec.pc)
file(READ ${BUILD_DIR}/lib/pkgconfig/libavcodec.pc avcodecPc)
if(avcodecPc MATCHES "rockchip_mpp")
@@ -857,9 +869,9 @@ endif()
# System libraries follow the static ones so their symbols resolve for everything above.
# Apple's linker has no --start-group, so the list is repeated there instead.
if(KANGAROO_OS STREQUAL "macos")
- target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${STATIC_LIBS} ${STATIC_LIBS} ${STATIC_LIBS} Jolt::Jolt RecastNavigation::DetourCrowd RecastNavigation::Detour RecastNavigation::Recast RmlUi::Lua RmlUi::Debugger RmlUi::Core ${SYSTEM_LIBS} -pthread -lm)
+ target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${STATIC_LIBS} ${STATIC_LIBS} ${STATIC_LIBS} Jolt::Jolt RecastNavigation::DetourCrowd RecastNavigation::Detour RecastNavigation::Recast RmlUi::Lua RmlUi::Core ${SYSTEM_LIBS} -pthread -lm)
else()
- target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE -Wl,--start-group ${STATIC_LIBS} -Wl,--end-group Jolt::Jolt RecastNavigation::DetourCrowd RecastNavigation::Detour RecastNavigation::Recast RmlUi::Lua RmlUi::Debugger RmlUi::Core ${SYSTEM_LIBS} -pthread -lm)
+ target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE -Wl,--start-group ${STATIC_LIBS} -Wl,--end-group Jolt::Jolt RecastNavigation::DetourCrowd RecastNavigation::Detour RecastNavigation::Recast RmlUi::Lua RmlUi::Core ${SYSTEM_LIBS} -pthread -lm)
endif()
diff --git a/LICENSES b/LICENSES
index d61ce50b2..edea0cbeb 100644
--- a/LICENSES
+++ b/LICENSES
@@ -67,3 +67,9 @@ Fonts
-----
FreeSansBold GPL-3.0 with font exception https://www.gnu.org/software/freefont
+
+
+Data
+----
+
+Mozilla CA list 20260601 (Debian ca-certificates) MPL-2.0 https://wiki.mozilla.org/CA (assets/cacerts.pem)
diff --git a/assets/Backdrop.singe b/assets/Backdrop.singe
index 045fb9eae..7c29b552e 100644
--- a/assets/Backdrop.singe
+++ b/assets/Backdrop.singe
@@ -134,6 +134,7 @@ local backdropStarted = 0 -- singeGetTicks() when the backdrop's clock began
local backdropTagX = 0
local backdropTagY = 0
local blown = false -- Whether the charge has gone off.
+local gridLit = false -- Whether the grid has finished coming up and its colours are final.
local backdropFont = nil
-- ===== The backdrop ==========================================================================
@@ -473,7 +474,6 @@ function backdropFrame()
emitterBurst(core, 110)
emitterBurst(flash, 5)
emitterBurst(sparks, 130)
- lightSetIntensity(fireLight, 400)
end
-- The fire's light dies over the first second and a half.
@@ -483,13 +483,15 @@ function backdropFrame()
-- there is no moment where one thing has ended and the next has not begun.
local lit = span(t, GRID_UP, GRID_LIT)
- if (lit > 0) and (lit < 1.0001) then
+ if (lit > 0) and not gridLit then
materialSetColor(gridMat, 255 * lit, 45 * lit, 190 * lit)
materialSetColor(horizMat, 255 * lit, 150 * lit, 235 * lit)
materialSetColor(sunMat2, 255 * lit, 130 * lit, 55 * lit)
-- The sky stops being black at the same time, or the grid would rise into a void.
sceneSetBackground(FOG_R * lit, FOG_G * lit, FOG_B * lit, 255)
sceneSetFog(FOG_R, FOG_G, FOG_B, FOG_NEAR, FOG_FAR)
+ -- Once fully lit the colours are final, so they are not written again every frame.
+ gridLit = (lit >= 1)
end
if lit > 0 then
-- Scrolling from the moment it is visible, so it is already alive when it arrives.
@@ -579,6 +581,7 @@ function backdropBegin(showIntro)
backdropStarted = singeGetTicks() - (showIntro and 0 or (LIFT_TO * 1000))
blown = not showIntro
+ gridLit = false
if not showIntro then
nodeSetVisible(logo, false)
end
diff --git a/assets/Forge/Author.singe b/assets/Forge/Author.singe
index 24555cafc..664c4c84d 100644
--- a/assets/Forge/Author.singe
+++ b/assets/Forge/Author.singe
@@ -25,7 +25,7 @@
-- The runtime an authored game is compiled against (PLAN section 58).
--
--- A game made with the authoring tools is not interpreted: Singe/AuthorCompile.singe turns its
+-- A game made with the authoring tools is not interpreted: Forge/AuthorCompile.singe turns its
-- description into ordinary Lua, and this file is the library that Lua calls. Rules become real
-- `if` statements, so nothing walks a table every frame and the result can be opened in ZeroBrane
-- and edited by hand like any other game.
@@ -163,7 +163,9 @@ AUTHOR.behaviours.solid = {
help = "Immovable ground or a wall.",
params = {},
attach = function(entity)
- bodyNew(entity.node, BODY_STATIC, SHAPE_BOX, entity.look.w / 2, entity.look.h / 2, entity.look.w / 2)
+ local w, h = authorSize(entity)
+
+ bodyNew(entity.node, BODY_STATIC, SHAPE_BOX, w / 2, h / 2, w / 2)
end
}
@@ -171,9 +173,11 @@ AUTHOR.behaviours.platformer = {
help = "Runs, falls and jumps: the engine's character controller in 2D.",
params = { speed = "number", jump = "number" },
attach = function(entity, params)
+ local w, h = authorSize(entity)
+
entity.speed = params.speed or 200
entity.jump = params.jump or 500
- playerNew(entity.node, SHAPE_CAPSULE, entity.look.w * PLAYER_RADIUS, entity.look.h)
+ playerNew(entity.node, SHAPE_CAPSULE, w * PLAYER_RADIUS, h)
end,
step = function(entity)
-- The rules say which way; this clears it each frame so releasing a key stops the run.
@@ -205,7 +209,7 @@ AUTHOR.behaviours.drift = {
AUTHOR.conditions.keyHeld = {
help = "A key is down.",
params = { key = "scancode" },
- emit = function(p) return string.format("authorKeyHeld(SCANCODE.%s)", p.key) end
+ emit = function(p) return string.format("authorKeyHeld(SCANCODE.%s.value)", p.key) end
}
AUTHOR.conditions.switchHeld = {
@@ -364,13 +368,27 @@ end
function authorTouching(idA, idB)
- local a = AUTHOR_WORLD[idA]
- local b = AUTHOR_WORLD[idB]
+ local a = AUTHOR_WORLD[idA]
+ local b = AUTHOR_WORLD[idB]
local ax, ay = authorPosition(a)
local bx, by = authorPosition(b)
+ local aw, ah = authorSize(a)
+ local bw, bh = authorSize(b)
- return collideRects(ax - a.look.w / 2, ay - a.look.h / 2, a.look.w, a.look.h,
- bx - b.look.w / 2, by - b.look.h / 2, b.look.w, b.look.h)
+ return collideRects(ax - aw / 2, ay - ah / 2, aw, ah, bx - bw / 2, by - bh / 2, bw, bh)
+end
+
+
+-- How big an entity is, for touching and for the body a behaviour gives it. A box says; a sprite
+-- is its image; a text look has no size of its own and counts as a point unless it was given one.
+function authorSize(entity)
+ local look = entity.look
+
+ if entity.sprite ~= nil then
+ return spriteGetWidth(entity.sprite), spriteGetHeight(entity.sprite)
+ end
+
+ return look.w or 0, look.h or 0
end
@@ -431,9 +449,17 @@ function authorMake(description)
node = nodeNew()
}
local look = AUTHOR.looks[entity.look.kind]
- local b
nodeSetPosition(entity.node, description.x or 0, description.y or 0, 0)
+ if look == nil then
+ -- A look that does not exist would take the game down when it came to be drawn; a box
+ -- is what it wears instead, and the console says which entity it was.
+ debugPrint("Author: no look called '" .. tostring(entity.look.kind) .. "' on " .. tostring(entity.id) .. "; drawing a box")
+ entity.look.kind = "box"
+ entity.look.w = entity.look.w or 20
+ entity.look.h = entity.look.h or 20
+ look = AUTHOR.looks.box
+ end
if look.load then
look.load(entity)
end
@@ -458,8 +484,6 @@ end
-- Starts the layers the game declared. Anything a layer needs of the engine is asked for here and
-- nowhere else, which is what keeps the rest of this file free of genre.
function authorBegin(layers)
- local layer
-
-- A font, because the text look draws with fontPrint and fontPrint ends the game when none is
-- selected. Every test had a scene select one first; a released game has nobody to do that,
-- and died on its first text entity. A game that wants its own calls fontSelect afterwards.
@@ -489,12 +513,9 @@ end
function authorFrame(rules)
local now = authorTime()
local dt = now - lastTime
- local entity
lastTime = now
for _, entity in ipairs(AUTHOR_ORDER) do
- local b
-
for _, b in ipairs(entity.behaviours) do
local kind = AUTHOR.behaviours[b.kind]
diff --git a/assets/Forge/AuthorCompile.singe b/assets/Forge/AuthorCompile.singe
index 7ba0b481c..1801d658a 100644
--- a/assets/Forge/AuthorCompile.singe
+++ b/assets/Forge/AuthorCompile.singe
@@ -33,10 +33,14 @@
-- This is written in Lua rather than in util/ as a Python script because the editor is itself a
-- Singe game: it has to be able to compile what it is editing without leaving the engine. It
-- lives with Forge rather than in Singe/ because only building needs it -- a finished game is
--- already Lua. Singe/Author.singe, the runtime that game loads, has to stay an engine support
--- file: a game handed to someone who has never installed Forge still has to run.
+-- already Lua. Author.singe, the runtime that game loads, travels with the game: a copy is
+-- written beside it when it is built, so a game handed to someone who has never installed Forge
+-- still runs. The compiler loads it too, for the vocabulary it emits against.
-dofile("Singe/Author.singe")
+-- Where the runtime is taken from when a game is built. Forge carries it; a game gets a copy.
+AUTHOR_RUNTIME = "Forge/Author.singe"
+
+dofile(AUTHOR_RUNTIME)
local INDENT = "\t"
@@ -58,7 +62,6 @@ end
local function tableSource(t)
local parts = {}
local keys = {}
- local key
for key in pairs(t) do
keys[#keys + 1] = key
@@ -88,7 +91,6 @@ end
local function emitConditions(rule)
local tests = {}
- local item
for _, item in ipairs(rule.when or {}) do
local test = emitOne(AUTHOR.conditions, item, "condition")
@@ -108,8 +110,6 @@ end
local function emitRule(out, rule, index)
- local item
-
out[#out + 1] = string.format("%s-- %s", INDENT, rule.note or ("rule " .. index))
out[#out + 1] = string.format("%sif %s then", INDENT, emitConditions(rule))
for _, item in ipairs(rule.act or {}) do
@@ -131,11 +131,8 @@ end
-- the engine's, as it is for every game ever written for Singe.
function authorCompile(game)
local out = {}
- local entity
- local layer
- local rule
- out[#out + 1] = "-- Generated by Singe/AuthorCompile.singe from " .. (game.source or "a game description")
+ out[#out + 1] = "-- Generated by Forge/AuthorCompile.singe from " .. (game.source or "a game description")
out[#out + 1] = "-- " .. (game.title or "Untitled")
out[#out + 1] = "--"
out[#out + 1] = "-- This is an ordinary Singe game and can be edited by hand. Doing so and then"
@@ -160,7 +157,6 @@ function authorCompile(game)
for _, entity in ipairs(game.entities or {}) do
local parts = {}
- local b
parts[#parts + 1] = string.format("id = %q", entity.id)
parts[#parts + 1] = string.format("x = %s, y = %s", entity.x or 0, entity.y or 0)
@@ -226,10 +222,6 @@ function authorCopy(fromPath, toPath)
end
--- Where the runtime is taken from when a game is built. Forge carries it; a game gets a copy.
-AUTHOR_RUNTIME = "Forge/Author.singe"
-
-
-- The directory part of a path, with its separator, or "" for a bare name.
local function directoryOf(path)
return (string.match(path, "^(.*[/\\])") or "")
@@ -267,9 +259,6 @@ end
-- Whether a table holds only leaves, in which case it is written on one line. A rule reads far
-- better as { "keyHeld", key = "LEFT" } than as six lines, and it is what an author typed.
local function isLeaf(t)
- local key
- local value
-
for key, value in pairs(t) do
if type(value) == "table" then
return false
@@ -288,8 +277,6 @@ local function valueSource(value, depth)
local parts = {}
local keys = {}
local count = 0
- local key
- local item
local sep
local open
local close
@@ -340,7 +327,6 @@ end
function authorSave(game, path)
local file = assert(io.open(path, "w"))
local copy = {}
- local key
-- source says where a description came from and is set by the loader, so writing it back out
-- would make a description that had been through the editor differ from one that had not.
@@ -349,7 +335,7 @@ function authorSave(game, path)
copy[key] = game[key]
end
end
- file:write("-- Written by Singe/AuthorCompile.singe. Edit by hand or in the editor; either is fine.\n")
+ file:write("-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.\n")
file:write("return " .. valueSource(copy, 0) .. "\n")
file:close()
diff --git a/assets/Forge/Forge.singe b/assets/Forge/Forge.singe
index bbd13c506..34e07d60f 100644
--- a/assets/Forge/Forge.singe
+++ b/assets/Forge/Forge.singe
@@ -23,6 +23,7 @@
+
-- Forge: the authoring tool (PLAN section 58).
--
-- It is a Singe game. Nothing here is a preview: the canvas is the same overlay, at the same
@@ -33,6 +34,11 @@
-- collidePointRect, which is what Singe has always done with a pointer and a set of rectangles.
-- They compose because the engine offers a button to the GUI first and passes on what it did not
-- use, while pointer motion is never consumed at all.
+--
+-- The file is two things. Everything up to the last section is a library: a scene that drives
+-- the editor sets FORGE_LIBRARY before loading it and supplies its own callbacks. The last section
+-- is Forge as a game -- the file chooser, the engine callbacks and the play button -- and runs only
+-- when nothing has said otherwise.
-- Framework is wanted for SCANCODE, which the key handling below names; AuthorCompile brings in
-- the runtime and the manifest. A generated game loads Framework for itself, so the editor only
@@ -44,28 +50,38 @@ local lfs = require("lfs")
FORGE = {
- game = nil, -- The description being edited.
- path = nil,
- selected = nil, -- Index into game.entities.
- dirty = false,
- gui = nil,
- document = nil,
- held = nil, -- The entity being dragged.
- grabX = 0,
- grabY = 0,
- panelX = 0, -- The chrome slides sideways so nothing is permanently under it.
- panelDrag = nil, -- Offset from the panel's left edge while it is being dragged.
- mode = "entities", -- or "rules". The panel is narrow; one list at a time reads better.
- rule = 1, -- Selected rule.
- part = 0, -- Selected condition or action within it; 0 is the rule itself.
- message = "loaded"
+ game = nil, -- The description being edited.
+ path = nil,
+ selected = nil, -- Index into game.entities.
+ dirty = false,
+ gui = nil,
+ document = nil,
+ held = nil, -- The entity being dragged.
+ grabX = 0,
+ grabY = 0,
+ panelX = 0, -- The chrome slides sideways so nothing is permanently under it.
+ panelDrag = nil, -- Offset from the panel's left edge while it is being dragged.
+ mode = "entities", -- or "rules". The panel is narrow; one list at a time reads better.
+ rule = 1, -- Selected rule.
+ part = 0, -- Selected condition or action within it; 0 is the rule itself.
+ lastField = nil, -- The parameter ENTER edited last, so the next ENTER moves on from it.
+ editing = nil, -- { key, text, was, set }: see "Typing a value".
+ picker = nil, -- A list being chosen from: see "Pickers".
+ undo = {}, -- Copies of the description, newest last.
+ redo = {}, -- What undo took away, newest last; any other change empties it.
+ sprites = {}, -- Sprite looks loaded for drawing, by file name; false when missing.
+ closing = false, -- ESC has been pressed once on an unsaved description.
+ message = "loaded"
}
-local PANEL_W = 190 -- Matches AuthorEdit.rml; the canvas starts to the right of it.
-local HANDLE = 6 -- Half the size of the square drawn at an entity's own position.
-local TAB_W = 16 -- The panel's drag tab, drawn on the canvas just outside its edge.
-local TAB_H = 54
-local TAB_Y = 10
+local PANEL_W = 190 -- Matches Forge.rml; the canvas starts to the right of it.
+local HANDLE = 6 -- Half the size of the square drawn at an entity's own position.
+local TAB_W = 16 -- The panel's drag tab, drawn on the canvas just outside its edge.
+local TAB_H = 54
+local TAB_Y = 10
+local UNDO_MAX = 40 -- Steps kept. A description is small; forty copies are nothing.
+local NEW_W = 40 -- The box a new entity wears until it is given a look.
+local NEW_H = 40
local function element(id)
@@ -73,26 +89,51 @@ local function element(id)
end
+-- A sprite look's image, loaded once and kept, so the canvas can show it. A file that is not
+-- there answers nil rather than ending the editor the way spriteLoad would: the entity is drawn
+-- as a box until the name is right.
+local function spriteFor(file)
+ if FORGE.sprites[file] == nil then
+ if (file ~= "") and (lfs.attributes(file, "mode") == "file") then
+ FORGE.sprites[file] = spriteLoad(file)
+ else
+ FORGE.sprites[file] = false
+ end
+ end
+
+ return FORGE.sprites[file] or nil
+end
+
+
-- An entity's box in overlay coordinates. A look without a size still gets one, so that a text
--- entity can be picked up and moved like anything else.
+-- entity can be picked up and moved like anything else; a sprite is as big as its image.
function forgeBounds(entity)
- local w = (entity.look and entity.look.w) or 90
- local h = (entity.look and entity.look.h) or 20
+ local look = entity.look or {}
+ local w = look.w or 90
+ local h = look.h or 20
+
+ if look.kind == "sprite" then
+ local sprite = spriteFor(look.file or "")
+
+ if sprite ~= nil then
+ w = spriteGetWidth(sprite)
+ h = spriteGetHeight(sprite)
+ end
+ end
return entity.x - w / 2, entity.y - h / 2, w, h
end
function forgeSelect(index)
- FORGE.selected = index
+ FORGE.selected = index
+ FORGE.lastField = nil
forgeRefresh()
end
-- The topmost entity under a point, so overlapping things pick the one drawn last.
function forgePick(x, y)
- local index
-
for index = #FORGE.game.entities, 1, -1 do
local bx, by, bw, bh = forgeBounds(FORGE.game.entities[index])
@@ -118,25 +159,41 @@ local function escape(text)
end
-local function refreshEntities()
+-- The fields of whatever is selected, one row each, with the one ENTER is on marked. This is the
+-- detail panel for an entity and for a condition or action alike: the same rows, the same keys.
+local function fieldRows(fields)
local rows = {}
- local index
+
+ for _, name in ipairs(fields.names) do
+ local class = (name == FORGE.lastField) and "part here" or "part"
+
+ rows[#rows + 1] = string.format('
%s %s
', class, escape(name), escape(tostring(fields.get(name))))
+ end
+
+ return table.concat(rows)
+end
+
+
+local function refreshEntities()
+ local rows = {}
+ local fields = forgeFields()
for index, entity in ipairs(FORGE.game.entities) do
local class = (index == FORGE.selected) and "row selected" or "row"
rows[#rows + 1] = string.format('%s
', index, class, escape(entity.id))
end
+ if #rows == 0 then
+ rows[1] = 'no entities yet: A adds one
'
+ end
element("list").inner_rml = table.concat(rows)
for index = 1, #FORGE.game.entities do
guiSetHandler(FORGE.gui, FORGE.document, "e" .. index, "click", function()
forgeSelect(index)
end)
end
- if FORGE.selected ~= nil then
- local entity = FORGE.game.entities[FORGE.selected]
-
- element("detail").inner_rml = string.format("%s
x %d y %d
%s", escape(entity.id), entity.x or 0, entity.y or 0, escape(FORGE.message))
+ if fields ~= nil then
+ element("detail").inner_rml = fieldRows(fields) .. '' .. escape(FORGE.message) .. "
"
else
element("detail").inner_rml = "nothing selected
" .. escape(FORGE.message)
end
@@ -146,9 +203,8 @@ end
-- The event sheet. The selected rule is opened in place and its conditions and actions listed
-- under it, because a rule only means anything as a whole: seeing "when" without "then" is no use.
local function refreshRules()
- local rows = {}
- local index
- local rule
+ local rows = {}
+ local fields = forgeFields()
for index, rule in ipairs(FORGE.game.rules or {}) do
local class = (index == FORGE.rule) and "row selected" or "row"
@@ -156,7 +212,6 @@ local function refreshRules()
rows[#rows + 1] = string.format('%d. %s
', class, index, escape(rule.note or "rule"))
if index == FORGE.rule then
local parts = forgeParts(rule)
- local at
for at, part in ipairs(parts) do
local mark = (at == FORGE.part) and "part here" or "part"
@@ -164,15 +219,30 @@ local function refreshRules()
rows[#rows + 1] = string.format('%s %s
', mark, (part.kind == "when") and "when" or "then", escape(forgePartText(part.item)))
end
if #parts == 0 then
- rows[#rows + 1] = '(empty)
'
+ rows[#rows + 1] = '(empty: C adds a condition, T an action)
'
end
end
end
if #rows == 0 then
- rows[1] = 'no rules yet
'
+ rows[1] = 'no rules yet: N adds one
'
end
element("list").inner_rml = table.concat(rows)
- element("detail").inner_rml = string.format("rule %d of %d
%s", FORGE.rule, #(FORGE.game.rules or {}), escape(FORGE.message))
+ element("detail").inner_rml = string.format("rule %d of %d
%s%s", FORGE.rule, #(FORGE.game.rules or {}), (fields ~= nil) and fieldRows(fields) or "", '' .. escape(FORGE.message) .. "
")
+end
+
+
+-- A picker takes the list over while it is up: the vocabulary is what is being chosen from, and
+-- the panel is too narrow to show it beside anything else.
+local function refreshPicker()
+ local rows = {}
+
+ for at, item in ipairs(FORGE.picker.items) do
+ local class = (at == FORGE.picker.at) and "row selected" or "row"
+
+ rows[#rows + 1] = string.format('%s
', class, escape(item.name))
+ end
+ element("list").inner_rml = table.concat(rows)
+ element("detail").inner_rml = escape(FORGE.picker.title) .. "
" .. escape(FORGE.picker.items[FORGE.picker.at].help or "") .. 'UP / DOWN choose, ENTER takes it, ESC leaves it
'
end
@@ -182,7 +252,9 @@ function forgeRefresh()
end
element("title").inner_rml = escape(FORGE.game.title or "Untitled") .. (FORGE.dirty and " *" or "")
element("grip").inner_rml = (FORGE.mode == "rules") and "≡ Rules" or "≡ Entities"
- if FORGE.mode == "rules" then
+ if FORGE.picker ~= nil then
+ refreshPicker()
+ elseif FORGE.mode == "rules" then
refreshRules()
else
refreshEntities()
@@ -196,8 +268,22 @@ function forgeBegin(path)
if FORGE.game == nil then
return false
end
- FORGE.gui = guiNew(overlayGetWidth(), overlayGetHeight())
- FORGE.document = guiLoad(FORGE.gui, "Forge/Forge.rml")
+ FORGE.game.entities = FORGE.game.entities or {}
+ FORGE.game.rules = FORGE.game.rules or {}
+ FORGE.game.layers = FORGE.game.layers or {}
+ FORGE.selected = nil
+ FORGE.rule = 1
+ FORGE.part = 0
+ FORGE.lastField = nil
+ FORGE.editing = nil
+ FORGE.picker = nil
+ FORGE.undo = {}
+ FORGE.redo = {}
+ FORGE.dirty = false
+ FORGE.closing = false
+ FORGE.message = "loaded"
+ FORGE.gui = guiNew(overlayGetWidth(), overlayGetHeight())
+ FORGE.document = guiLoad(FORGE.gui, "Forge/Forge.rml")
guiSetInput(FORGE.gui, true)
guiSetHandler(FORGE.gui, FORGE.document, "grip", "mousedown", function()
local x = mouseGetPosition(0)
@@ -216,6 +302,29 @@ function forgeBegin(path)
end
+-- Puts the editor away. The description is dropped, saved or not: the caller decides that first.
+function forgeClose()
+ if FORGE.gui ~= nil then
+ guiDelete(FORGE.gui)
+ end
+ for _, sprite in pairs(FORGE.sprites) do
+ if sprite then
+ spriteUnload(sprite)
+ end
+ end
+ FORGE.gui = nil
+ FORGE.document = nil
+ FORGE.game = nil
+ FORGE.path = nil
+ FORGE.sprites = {}
+ FORGE.editing = nil
+ FORGE.picker = nil
+ FORGE.held = nil
+ FORGE.undo = {}
+ FORGE.redo = {}
+end
+
+
-- Moves the selected entity. The description is the truth; nothing is cached anywhere else, so
-- saving is only writing it back out.
function forgeMove(index, x, y)
@@ -231,6 +340,7 @@ end
function forgeSave()
authorSave(FORGE.game, FORGE.path)
FORGE.dirty = false
+ FORGE.closing = false
FORGE.message = "saved"
forgeRefresh()
@@ -285,11 +395,13 @@ function forgeExport(folder, name)
-- The description travels with the game, so the release can be opened and edited again.
authorSave(FORGE.game, folder .. "/" .. script .. ".game")
- -- A games.dat so the menu lists it, written from what the description already knows.
+ -- A games.dat so the menu lists it, written from what the description already knows. A game
+ -- over a disc names its video in the description; anything else plays over the menu's, since
+ -- the engine wants one and the game draws over all of it anyway.
dat = io.open(folder .. "/games.dat", "w")
if dat ~= nil then
- dat:write(string.format("-- Written by Forge.\nGAMES = {\n\t{\n\t\tTITLE = %q,\n\t\tSCRIPT = %q,\n\t\tDATA = %q,\n\t\tVIDEO = \"Singe/menuBackground.mkv\",\n\t\tRESOLUTION_X = %d,\n\t\tRESOLUTION_Y = %d\n\t}\n}\n",
- title, folder .. "/" .. script .. ".singe", folder, overlayGetWidth(), overlayGetHeight()))
+ dat:write(string.format("-- Written by Forge.\nGAMES = {\n\t{\n\t\tTITLE = %q,\n\t\tSCRIPT = %q,\n\t\tVIDEO = %q,\n\t\tRESOLUTION_X = %d,\n\t\tRESOLUTION_Y = %d\n\t}\n}\n",
+ title, folder .. "/" .. script .. ".singe", forgeVideo(), overlayGetWidth(), overlayGetHeight()))
dat:close()
end
@@ -300,13 +412,21 @@ function forgeExport(folder, name)
end
--- Compiles what is on screen and hands back the path, so the caller can dofile it and play.
+-- The video a description plays over. Its own when it names one, the menu's otherwise.
+function forgeVideo()
+ return FORGE.game.video or "Singe/menuBackground.mkv"
+end
+
+
+-- Compiles what is on screen and hands back the path, so the caller can dofile it and play. The
+-- runtime goes beside it, because that is where the compiled game looks for it.
function forgeBuild(outputFile)
FORGE.game.source = FORGE.path
local file = assert(io.open(outputFile, "w"))
file:write(authorCompile(FORGE.game))
file:close()
+ authorCopy(AUTHOR_RUNTIME, (string.match(outputFile, "^(.*[/\\])") or "") .. "Author.singe")
FORGE.message = "built"
forgeRefresh()
@@ -332,8 +452,13 @@ function forgePress(x, y)
if FORGE.held ~= nil then
local entity = FORGE.game.entities[FORGE.held]
+ -- One undo step for the whole drag, taken before it starts.
+ forgeRemember()
FORGE.grabX = x - entity.x
FORGE.grabY = y - entity.y
+ if FORGE.mode ~= "entities" then
+ FORGE.mode = "entities"
+ end
forgeSelect(FORGE.held)
end
end
@@ -391,14 +516,12 @@ end
-- Draws the description. Note that this draws the *description*, not a running game: there are
-- no nodes here and no physics, which is why an entity can be dragged through a wall.
function forgeDraw(pointerX, pointerY)
- local index
-
colorBackground(18, 18, 26, 255)
overlayClear()
for index, entity in ipairs(FORGE.game.entities) do
local bx, by, bw, bh = forgeBounds(entity)
local look = entity.look or {}
- local row
+ local sprite = (look.kind == "sprite") and spriteFor(look.file or "") or nil
-- Everything is drawn, including whatever is under the chrome. The panel is opaque and
-- slides, so it covers rather than hides: drag it aside and what was beneath it is there,
@@ -410,6 +533,8 @@ function forgeDraw(pointerX, pointerY)
fontPrint(entity.x, entity.y, look.text)
end
overlayBox(bx, by, bx + bw, by + bh)
+ elseif sprite ~= nil then
+ spriteDraw(sprite, bx, by)
else
for row = math.floor(by), math.floor(by + bh) do
overlayLine(bx, row, bx + bw, row)
@@ -423,7 +548,6 @@ function forgeDraw(pointerX, pointerY)
end
-- The panel's drag tab.
local tx, ty, tw, th = forgeTab()
- local trow
colorForeground(70, 70, 96, 255)
for trow = ty, ty + th do
@@ -443,6 +567,334 @@ function forgeDraw(pointerX, pointerY)
end
+-- ===== Undo and redo ==========================================================================
+--
+-- The description is small and plain data, so undo is a copy of the whole thing taken before
+-- each change, and undoing is putting the last copy back. Nothing has to know how to reverse
+-- itself, which is what keeps every later feature from having to. Redo is the same the other
+-- way: what undo took out is kept until something else changes.
+
+local function copyOf(value)
+ local out
+
+ if type(value) ~= "table" then
+ return value
+ end
+ out = {}
+ for key, item in pairs(value) do
+ out[key] = copyOf(item)
+ end
+
+ return out
+end
+
+
+-- Keeps a copy on one of the two stacks, oldest dropped once it is full.
+local function keep(stack)
+ stack[#stack + 1] = copyOf(FORGE.game)
+ if #stack > UNDO_MAX then
+ table.remove(stack, 1)
+ end
+end
+
+
+-- Call before changing the description. A drag is one step, a typed value is one step. A change
+-- made by hand is the end of the redo history: what was undone before it no longer applies.
+function forgeRemember()
+ keep(FORGE.undo)
+ FORGE.redo = {}
+end
+
+
+-- Puts a copy back in place of the description, keeping the current one on the other stack.
+local function swapTo(copy, other, message)
+ keep(other)
+ FORGE.game = copy
+ FORGE.editing = nil -- Whatever was being typed referred to the description just replaced.
+ FORGE.held = nil
+ FORGE.selected = FORGE.selected and math.min(FORGE.selected, #FORGE.game.entities) or nil
+ if FORGE.selected == 0 then
+ FORGE.selected = nil
+ end
+ FORGE.rule = math.max(1, math.min(FORGE.rule, #FORGE.game.rules))
+ FORGE.part = math.min(FORGE.part, #forgeParts(FORGE.game.rules[FORGE.rule] or {}))
+ FORGE.dirty = true
+ FORGE.message = message
+ forgeRefresh()
+end
+
+
+function forgeUndo()
+ local was = table.remove(FORGE.undo)
+
+ if was == nil then
+ FORGE.message = "nothing to undo"
+ forgeRefresh()
+ return false
+ end
+ swapTo(was, FORGE.redo, "undone")
+
+ return true
+end
+
+
+function forgeRedo()
+ local again = table.remove(FORGE.redo)
+
+ if again == nil then
+ FORGE.message = "nothing to redo"
+ forgeRefresh()
+ return false
+ end
+ swapTo(again, FORGE.undo, "redone")
+
+ return true
+end
+
+
+-- ===== Entities ===============================================================================
+--
+-- An entity is a position, a look and a list of behaviours. All of it is edited the same way a
+-- rule's parameters are: ENTER walks the fields, and each is typed. The fields come from the
+-- manifest -- a look's parameters, a behaviour's parameters -- so a new look or behaviour is
+-- editable the moment it is declared.
+
+-- A name no other entity has, from a stem.
+local function uniqueId(stem)
+ local taken = {}
+ local n = 1
+
+ for _, entity in ipairs(FORGE.game.entities) do
+ taken[entity.id] = true
+ end
+ if not taken[stem] then
+ return stem
+ end
+ while taken[stem .. n] do
+ n = n + 1
+ end
+
+ return stem .. n
+end
+
+
+-- A new box in the middle of the canvas, clear of the panel, selected and ready to be moved.
+function forgeEntityAdd(x, y)
+ local entity = {
+ id = uniqueId("thing"),
+ x = math.floor(x or ((FORGE.panelX + PANEL_W + overlayGetWidth()) / 2)),
+ y = math.floor(y or (overlayGetHeight() / 2)),
+ look = { kind = "box", w = NEW_W, h = NEW_H, r = 180, g = 180, b = 190 },
+ behaviours = {}
+ }
+
+ forgeRemember()
+ FORGE.game.entities[#FORGE.game.entities + 1] = entity
+ FORGE.dirty = true
+ FORGE.message = "added " .. entity.id
+ forgeSelect(#FORGE.game.entities)
+
+ return #FORGE.game.entities
+end
+
+
+-- A copy of the selected entity, a little to one side so the two can be told apart.
+function forgeEntityDuplicate()
+ local entity = FORGE.game.entities[FORGE.selected]
+ local twin
+
+ if entity == nil then
+ return nil
+ end
+ forgeRemember()
+ twin = copyOf(entity)
+ twin.id = uniqueId(entity.id)
+ twin.x = entity.x + 20
+ twin.y = entity.y + 20
+ FORGE.game.entities[#FORGE.game.entities + 1] = twin
+ FORGE.dirty = true
+ FORGE.message = "copied " .. entity.id .. " as " .. twin.id
+ forgeSelect(#FORGE.game.entities)
+
+ return #FORGE.game.entities
+end
+
+
+function forgeEntityDelete()
+ local entity = FORGE.game.entities[FORGE.selected]
+
+ if entity == nil then
+ return false
+ end
+ forgeRemember()
+ table.remove(FORGE.game.entities, FORGE.selected)
+ FORGE.dirty = true
+ FORGE.message = "deleted " .. entity.id
+ if #FORGE.game.entities == 0 then
+ forgeSelect(nil)
+ else
+ forgeSelect(math.min(FORGE.selected, #FORGE.game.entities))
+ end
+
+ return true
+end
+
+
+-- The sorted keys of a parameter table, so fields always read in the same order.
+local function sortedKeys(t)
+ local keys = {}
+
+ for key in pairs(t or {}) do
+ keys[#keys + 1] = key
+ end
+ table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
+
+ return keys
+end
+
+
+-- The behaviour of a kind an entity carries, if any.
+local function behaviourOf(entity, kind)
+ for _, b in ipairs(entity.behaviours or {}) do
+ if b.kind == kind then
+ return b
+ end
+ end
+
+ return nil
+end
+
+
+-- An entity's fields, in the order the panel shows them: what every entity has, then what its
+-- look takes, then its behaviours and what each of those takes. A behaviour's parameter is
+-- named kind.parameter, which is how it is typed as well.
+function forgeEntityFieldNames(entity)
+ local names = { "id", "x", "y", "kind" }
+ local look = AUTHOR.looks[(entity.look or {}).kind]
+
+ for _, key in ipairs(sortedKeys(look and look.params or entity.look)) do
+ if key ~= "kind" then
+ names[#names + 1] = key
+ end
+ end
+ names[#names + 1] = "behaviours"
+ for _, b in ipairs(entity.behaviours or {}) do
+ local kind = AUTHOR.behaviours[b.kind]
+
+ for _, key in ipairs(sortedKeys(kind and kind.params or b)) do
+ if key ~= "kind" then
+ names[#names + 1] = b.kind .. "." .. key
+ end
+ end
+ end
+
+ return names
+end
+
+
+function forgeEntityGet(entity, name)
+ local kind, key = string.match(name, "^([^.]+)%.(.+)$")
+
+ if (name == "id") or (name == "x") or (name == "y") then
+ return entity[name]
+ elseif name == "kind" then
+ return (entity.look or {}).kind
+ elseif name == "behaviours" then
+ local kinds = {}
+
+ for _, b in ipairs(entity.behaviours or {}) do
+ kinds[#kinds + 1] = b.kind
+ end
+ return table.concat(kinds, ", ")
+ elseif kind ~= nil then
+ local b = behaviourOf(entity, kind)
+
+ return b and b[key] or nil
+ end
+
+ return (entity.look or {})[name]
+end
+
+
+-- Renaming an entity renames it in every rule that talks about it, which is what the author meant.
+local function renameEverywhere(from, to)
+ for _, rule in ipairs(FORGE.game.rules) do
+ for _, list in ipairs({ rule.when or {}, rule.act or {} }) do
+ for _, item in ipairs(list) do
+ local entry = AUTHOR.conditions[item[1]] or AUTHOR.actions[item[1]]
+
+ for key, kind in pairs(entry and entry.params or {}) do
+ if (kind == "entity") and (item[key] == from) then
+ item[key] = to
+ end
+ end
+ end
+ end
+ end
+end
+
+
+-- Fills in whatever a look or a behaviour declares and the table does not yet have, so a kind
+-- that was just typed in has a value for every parameter and compiles at once.
+local function fillDefaults(t, params)
+ for key, kind in pairs(params or {}) do
+ if t[key] == nil then
+ t[key] = FORGE_DEFAULTS[kind]
+ end
+ end
+end
+
+
+function forgeEntitySet(entity, name, value)
+ local kind, key = string.match(name, "^([^.]+)%.(.+)$")
+
+ entity.look = entity.look or {}
+ if name == "id" then
+ if (value ~= "") and (value ~= entity.id) then
+ renameEverywhere(entity.id, value)
+ entity.id = value
+ end
+ elseif (name == "x") or (name == "y") then
+ if type(value) == "number" then
+ entity[name] = math.floor(value)
+ end
+ elseif name == "kind" then
+ if AUTHOR.looks[value] ~= nil then
+ entity.look.kind = value
+ fillDefaults(entity.look, AUTHOR.looks[value].params)
+ else
+ FORGE.message = "no look called " .. tostring(value)
+ end
+ elseif name == "behaviours" then
+ -- A comma separated list of kinds. One already carried keeps its values; a new one
+ -- starts with defaults; one left out goes.
+ local list = {}
+
+ for want in string.gmatch(tostring(value), "[^,%s]+") do
+ local b = behaviourOf(entity, want) or { kind = want }
+
+ if AUTHOR.behaviours[want] ~= nil then
+ fillDefaults(b, AUTHOR.behaviours[want].params)
+ list[#list + 1] = b
+ else
+ FORGE.message = "no behaviour called " .. want
+ end
+ end
+ entity.behaviours = list
+ elseif kind ~= nil then
+ local b = behaviourOf(entity, kind)
+
+ if b ~= nil then
+ b[key] = value
+ end
+ else
+ entity.look[name] = value
+ end
+ FORGE.dirty = true
+ forgeRefresh()
+end
+
+
-- ===== The event sheet ========================================================================
--
-- Rules are where someone with little programming skill actually spends their time, so this is the
@@ -454,7 +906,7 @@ end
-- The vocabulary comes from AUTHOR, so a rule editor never needs changing when a condition or an
-- action is added: it lists whatever the manifest declares.
-local DEFAULTS = {
+FORGE_DEFAULTS = {
number = 0,
string = "",
boolean = true,
@@ -470,7 +922,6 @@ local DEFAULTS = {
-- Every condition and action of a rule, flattened, so one index walks the whole thing.
function forgeParts(rule)
local parts = {}
- local item
for _, item in ipairs(rule.when or {}) do
parts[#parts + 1] = { kind = "when", item = item }
@@ -488,17 +939,15 @@ end
function forgeMakePart(set, name)
local entry = set[name]
local item = { name }
- local key
- local kind
if entry == nil then
return nil
end
for key, kind in pairs(entry.params or {}) do
if kind == "entity" and #FORGE.game.entities > 0 then
- item[key] = FORGE.game.entities[1].id
+ item[key] = FORGE.game.entities[FORGE.selected or 1].id
else
- item[key] = DEFAULTS[kind]
+ item[key] = FORGE_DEFAULTS[kind]
end
end
@@ -516,11 +965,12 @@ function forgeRuleAdd(kindName, name)
return false
end
local parts
- local at
+ forgeRemember()
rule[kindName] = rule[kindName] or {}
rule[kindName][#rule[kindName] + 1] = item
FORGE.dirty = true
+ FORGE.lastField = nil
FORGE.message = "added " .. name
-- Select what was just added, so its parameters can be set straight away. Conditions come
@@ -543,11 +993,11 @@ function forgePartDelete()
local parts = forgeParts(rule)
local part = parts[FORGE.part]
local list
- local i
if part == nil then
return false
end
+ forgeRemember()
list = rule[part.kind]
for i = 1, #list do
if list[i] == part.item then
@@ -583,9 +1033,11 @@ end
function forgeRuleNew(note)
+ forgeRemember()
FORGE.game.rules[#FORGE.game.rules + 1] = { note = note or "new rule", when = {}, act = {} }
FORGE.rule = #FORGE.game.rules
FORGE.part = 0
+ FORGE.lastField = nil
FORGE.dirty = true
FORGE.message = "rule added"
forgeRefresh()
@@ -598,6 +1050,7 @@ function forgeRuleDelete()
if FORGE.game.rules[FORGE.rule] == nil then
return false
end
+ forgeRemember()
table.remove(FORGE.game.rules, FORGE.rule)
FORGE.rule = math.max(1, math.min(FORGE.rule, #FORGE.game.rules))
FORGE.part = 0
@@ -609,20 +1062,32 @@ function forgeRuleDelete()
end
+-- Moves the selected rule up or down the sheet. Rules run in order, and "the readout, every
+-- frame" wants to be last, so the order is part of the game.
+function forgeRuleMove(by)
+ local rules = FORGE.game.rules
+ local to = FORGE.rule + by
+
+ if (rules[FORGE.rule] == nil) or (rules[to] == nil) then
+ return false
+ end
+ forgeRemember()
+ rules[FORGE.rule], rules[to] = rules[to], rules[FORGE.rule]
+ FORGE.rule = to
+ FORGE.dirty = true
+ FORGE.message = "rule moved"
+ forgeRefresh()
+
+ return true
+end
+
+
-- One condition or action as a line: its name, then its parameters in a fixed order so the same
-- rule always reads the same way.
function forgePartText(item)
- local keys = {}
- local out = {}
- local key
+ local out = {}
- for key in pairs(item) do
- if key ~= 1 then
- keys[#keys + 1] = key
- end
- end
- table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
- for _, key in ipairs(keys) do
+ for _, key in ipairs(forgeFieldNames(item)) do
out[#out + 1] = key .. " " .. tostring(item[key])
end
@@ -631,27 +1096,89 @@ end
function forgeMode(mode)
- FORGE.mode = mode
+ FORGE.mode = mode
+ FORGE.lastField = nil
+ FORGE.picker = nil
forgeRefresh()
end
--- Keys. Point the engine's callback at this and the editor is usable without a pointer at all,
--- which is how the bundled menu has always worked and what a cabinet needs.
+-- ===== Pickers ================================================================================
+--
+-- Choosing a condition, an action, a look or a behaviour is choosing from the manifest, and the
+-- manifest carries a line of help for each. The list takes the panel over while it is up.
+
+-- Opens a list. set is a manifest table (AUTHOR.conditions and its kind); choose is called with
+-- the name picked.
+function forgePickBegin(title, set, choose)
+ local items = {}
+
+ for _, name in ipairs(sortedKeys(set)) do
+ items[#items + 1] = { name = name, help = set[name].help }
+ end
+ if #items == 0 then
+ FORGE.message = "nothing to choose from"
+ forgeRefresh()
+ return false
+ end
+ FORGE.picker = { title = title, items = items, at = 1, choose = choose }
+ forgeRefresh()
+
+ return true
+end
+
+
+-- A key while a list is up. Answers whether it was taken.
+function forgePickKey(scancode)
+ local picker = FORGE.picker
+
+ if picker == nil then
+ return false
+ end
+ if scancode == SCANCODE.DOWN.value then
+ picker.at = math.min(picker.at + 1, #picker.items)
+ elseif scancode == SCANCODE.UP.value then
+ picker.at = math.max(picker.at - 1, 1)
+ elseif scancode == SCANCODE.RETURN.value then
+ FORGE.picker = nil
+ picker.choose(picker.items[picker.at].name)
+ elseif scancode == SCANCODE.ESCAPE.value then
+ FORGE.picker = nil
+ FORGE.message = "left it"
+ else
+ return true
+ end
+ forgeRefresh()
+
+ return true
+end
+
+
+-- ===== Keys ===================================================================================
+--
+-- Point the engine's callback at this and the editor is usable without a pointer at all, which is
+-- how the bundled menu has always worked and what a cabinet needs.
--
-- TAB entities or rules
-- UP / DOWN move within the list
-- LEFT/RIGHT move the panel out of the way
--- ENTER type a value for the selected condition or action; again for its next value
--- ESC put the value back
--- N new rule DELETE delete the selected rule or part
--- S save B build
+-- ENTER type a value for what is selected; again for its next value
+-- ESC put the value back; otherwise close the description
+-- A add an entity D duplicate it
+-- N new rule C / T add a condition / an action from the list
+-- [ / ] move the rule up / down the sheet
+-- DELETE delete the selected entity, rule, condition or action
+-- U / R undo / redo S save
+-- B build P play (Forge as a game only)
function forgeKey(keysym, scancode)
- local rules = FORGE.game.rules or {}
+ local rules = FORGE.game.rules
-- SCANCODE entries are tables of { name, value } and the engine hands onKeyPressed two plain
-- integers, so the comparison is against .value. Comparing against the table itself is always
-- false with a real keyboard; it only appeared to work because a test passed the table.
+ if forgePickKey(scancode) then
+ return
+ end
if FORGE.editing ~= nil then
if scancode == SCANCODE.RETURN.value then
forgeEditNext()
@@ -668,6 +1195,9 @@ function forgeKey(keysym, scancode)
forgeEditNext()
return
end
+ if scancode ~= SCANCODE.ESCAPE.value then
+ FORGE.closing = false
+ end
if scancode == SCANCODE.TAB.value then
forgeMode((FORGE.mode == "rules") and "entities" or "rules")
@@ -682,6 +1212,12 @@ function forgeKey(keysym, scancode)
forgeSelect(math.min((FORGE.selected or 0) + 1, #FORGE.game.entities))
elseif scancode == SCANCODE.UP.value then
forgeSelect(math.max((FORGE.selected or 2) - 1, 1))
+ elseif scancode == SCANCODE.A.value then
+ forgeEntityAdd()
+ elseif scancode == SCANCODE.D.value then
+ forgeEntityDuplicate()
+ elseif scancode == SCANCODE.DELETE.value then
+ forgeEntityDelete()
end
else
@@ -709,6 +1245,18 @@ function forgeKey(keysym, scancode)
forgeRefresh()
elseif scancode == SCANCODE.N.value then
forgeRuleNew()
+ elseif scancode == SCANCODE.C.value then
+ if rules[FORGE.rule] ~= nil then
+ forgePickBegin("a condition", AUTHOR.conditions, function(name) forgeRuleAdd("when", name) end)
+ end
+ elseif scancode == SCANCODE.T.value then
+ if rules[FORGE.rule] ~= nil then
+ forgePickBegin("an action", AUTHOR.actions, function(name) forgeRuleAdd("act", name) end)
+ end
+ elseif scancode == SCANCODE.LEFTBRACKET.value then
+ forgeRuleMove(-1)
+ elseif scancode == SCANCODE.RIGHTBRACKET.value then
+ forgeRuleMove(1)
elseif scancode == SCANCODE.DELETE.value then
if FORGE.part > 0 then
forgePartDelete()
@@ -722,6 +1270,10 @@ function forgeKey(keysym, scancode)
forgeSave()
elseif scancode == SCANCODE.B.value then
forgeBuild(singeGetDataPath() .. "preview.singe")
+ elseif scancode == SCANCODE.U.value then
+ forgeUndo()
+ elseif scancode == SCANCODE.R.value then
+ forgeRedo()
end
end
@@ -732,16 +1284,14 @@ end
-- useless, which is the half of the job that matters for someone with little programming skill.
--
-- Keysyms arrive as characters the way Tools.singe takes them, so this needs nothing of the GUI:
--- ENTER starts editing the selected condition or action, each press moves to its next parameter,
--- and ESCAPE puts back what was there.
+-- ENTER starts editing whatever is selected, each press moves to its next field, and ESCAPE puts
+-- back what was there. An entity and a condition are typed the same way; forgeFields says what
+-- the fields are and how to reach them.
-FORGE.editing = nil -- { key = name, text = typed so far, was = the value it started as }
-
-
--- The parameters of a part, in the order the panel shows them, so typing walks them the same way.
+-- The parameters of a condition or action, in the order the panel shows them, so typing walks them
+-- the same way.
function forgeFieldNames(item)
local keys = {}
- local key
for key in pairs(item) do
if key ~= 1 then
@@ -754,6 +1304,51 @@ function forgeFieldNames(item)
end
+-- What ENTER edits: the fields of whatever is selected, with a way to read and write each. nil
+-- when nothing is.
+function forgeFields()
+ if FORGE.mode == "rules" then
+ local rule = FORGE.game.rules[FORGE.rule]
+ local part
+
+ if rule == nil then
+ return nil
+ end
+ if FORGE.part == 0 then
+ return {
+ names = { "note" },
+ get = function(key) return rule[key] end,
+ set = function(key, value)
+ rule[key] = value
+ FORGE.dirty = true
+ forgeRefresh()
+ end
+ }
+ end
+ part = forgeParts(rule)[FORGE.part]
+ if part == nil then
+ return nil
+ end
+ return {
+ names = forgeFieldNames(part.item),
+ get = function(key) return part.item[key] end,
+ set = forgePartSet
+ }
+ end
+ local entity = FORGE.game.entities[FORGE.selected]
+
+ if entity == nil then
+ return nil
+ end
+
+ return {
+ names = forgeEntityFieldNames(entity),
+ get = function(key) return forgeEntityGet(entity, key) end,
+ set = function(key, value) forgeEntitySet(entity, key, value) end
+ }
+end
+
+
-- A typed value as the description should hold it. A number has to come back a number: "220" and
-- 220 compile to different source, and a description that changed shape because a value was
-- retyped would stop round-tripping.
@@ -781,7 +1376,7 @@ function forgeEditCommit()
if FORGE.editing == nil then
return false
end
- forgePartSet(FORGE.editing.key, typedValue(FORGE.editing.text, FORGE.editing.was))
+ FORGE.editing.set(FORGE.editing.key, typedValue(FORGE.editing.text, FORGE.editing.was))
FORGE.editing = nil
return true
@@ -792,46 +1387,45 @@ function forgeEditCancel()
if FORGE.editing == nil then
return false
end
- forgePartSet(FORGE.editing.key, FORGE.editing.was)
- FORGE.editing = nil
- FORGE.message = "unchanged"
+ FORGE.editing.set(FORGE.editing.key, FORGE.editing.was)
+ FORGE.editing = nil
+ FORGE.lastField = nil -- The next ENTER starts the form over rather than moving on.
+ FORGE.message = "unchanged"
forgeRefresh()
return true
end
--- Starts editing, or moves on to the next parameter of the same part. Committing as it moves is
+-- Starts editing, or moves on to the next field of the same thing. Committing as it moves is
-- what makes ENTER, ENTER, ENTER feel like filling in a form.
function forgeEditNext()
- local rule = FORGE.game.rules[FORGE.rule]
- local parts = forgeParts(rule or {})
- local part = parts[FORGE.part]
- local names
- local at = 0
- local i
+ local fields = forgeFields()
+ local at = 0
- if part == nil then
+ if fields == nil then
return false
end
- names = forgeFieldNames(part.item)
- if #names == 0 then
- FORGE.message = part.item[1] .. " takes no values"
+ if #fields.names == 0 then
+ FORGE.message = "nothing to type here"
forgeRefresh()
return false
end
if FORGE.editing ~= nil then
forgeEditCommit()
+ else
+ -- One undo step for the whole form, taken when typing starts.
+ forgeRemember()
end
- for i = 1, #names do
- if names[i] == FORGE.lastField then
+ for i = 1, #fields.names do
+ if fields.names[i] == FORGE.lastField then
at = i
end
end
- at = (at % #names) + 1
- FORGE.lastField = names[at]
- FORGE.editing = { key = names[at], text = "", was = part.item[names[at]] }
- FORGE.message = "type a value for " .. names[at] .. ", ENTER for the next, ESC to put it back"
+ at = (at % #fields.names) + 1
+ FORGE.lastField = fields.names[at]
+ FORGE.editing = { key = fields.names[at], text = "", was = fields.get(fields.names[at]), set = fields.set }
+ FORGE.message = "type a value for " .. fields.names[at] .. ", ENTER for the next, ESC to put it back"
forgeRefresh()
return true
@@ -851,9 +1445,324 @@ function forgeTyped(keysym)
return false
end
-- Shown as it is typed, so the value in the list is the value being entered.
- forgePartSet(FORGE.editing.key, (FORGE.editing.text == "") and FORGE.editing.was or typedValue(FORGE.editing.text, FORGE.editing.was))
+ FORGE.editing.set(FORGE.editing.key, (FORGE.editing.text == "") and FORGE.editing.was or typedValue(FORGE.editing.text, FORGE.editing.was))
FORGE.message = FORGE.editing.key .. " = " .. FORGE.editing.text .. "_"
forgeRefresh()
return true
end
+
+
+-- ===== Files ==================================================================================
+--
+-- Descriptions live in Forge's data directory, which is the one place a packed Forge can write.
+-- Ones dropped into Forge's own directory are offered too, and opened as a copy in the data
+-- directory, since inside a .game they are read only.
+
+-- A game to start from: ground, a hero that runs and jumps, and a readout. Enough to press play
+-- on, which is the point; everything in it can be changed or deleted.
+function forgeTemplate()
+ return {
+ title = "New game",
+ layers = { { kind = "world2d", gravity = 1500 } },
+ entities = {
+ { id = "ground", x = 360, y = 440, look = { kind = "box", w = 720, h = 40, r = 60, g = 70, b = 90 },
+ behaviours = { { kind = "solid" } } },
+ { id = "hero", x = 120, y = 380, look = { kind = "box", w = 24, h = 44, r = 230, g = 90, b = 170 },
+ behaviours = { { kind = "platformer", speed = 210, jump = 620 } } },
+ { id = "readout", x = 12, y = 12, look = { kind = "text", text = "score 0", r = 235, g = 235, b = 245 } }
+ },
+ rules = {
+ { note = "Run left", when = { { "keyHeld", key = "LEFT" } }, act = { { "run", entity = "hero", direction = -1 } } },
+ { note = "Run right", when = { { "keyHeld", key = "RIGHT" } }, act = { { "run", entity = "hero", direction = 1 } } },
+ { note = "Jump, with ground underfoot", when = { { "keyHeld", key = "SPACE" }, { "onGround", entity = "hero" } }, act = { { "jump", entity = "hero" } } },
+ { note = "The readout, every frame", when = {}, act = { { "setText", entity = "readout", text = '"score " .. AUTHOR_SCORE' } } }
+ }
+ }
+end
+
+
+-- Every .game description in a directory, sorted. A directory that is not there lists nothing.
+local function descriptionsIn(directory)
+ local found = {}
+
+ if lfs.attributes(directory, "mode") ~= "directory" then
+ return found
+ end
+ for name in lfs.dir(directory) do
+ if string.sub(name, -5) == ".game" then
+ found[#found + 1] = { name = string.sub(name, 1, -6), path = directory .. name }
+ end
+ end
+ table.sort(found, function(a, b) return a.name < b.name end)
+
+ return found
+end
+
+
+-- What the chooser offers: the data directory's descriptions, then Forge's own, then a new game.
+function forgeFiles()
+ local files = descriptionsIn(singeGetDataPath())
+
+ for _, sample in ipairs(descriptionsIn("Forge/")) do
+ sample.sample = true
+ files[#files + 1] = sample
+ end
+ files[#files + 1] = { name = "New game", fresh = true }
+
+ return files
+end
+
+
+-- A path in the data directory no description has yet.
+local function freshPath(stem)
+ local base = singeGetDataPath() .. stem
+ local n = 1
+
+ if lfs.attributes(base .. ".game") == nil then
+ return base .. ".game"
+ end
+ while lfs.attributes(base .. n .. ".game") ~= nil do
+ n = n + 1
+ end
+
+ return base .. n .. ".game"
+end
+
+
+-- Opens an entry from forgeFiles. A sample is copied first; a new game is written first.
+function forgeOpen(entry)
+ local path = entry.path
+
+ if entry.fresh then
+ path = freshPath("NewGame")
+ authorSave(forgeTemplate(), path)
+ elseif entry.sample then
+ path = freshPath(entry.name)
+ if not authorCopy(entry.path, path) then
+ return false
+ end
+ end
+ if not forgeBegin(path) then
+ return false
+ end
+ if entry.sample then
+ FORGE.message = "opened a copy: " .. path
+ elseif entry.fresh then
+ FORGE.message = "new game at " .. path
+ end
+ forgeRefresh()
+
+ return true
+end
+
+
+-- ===== Forge as a game ========================================================================
+--
+-- Everything above is a library; this is what runs when Forge is started from the menu. A scene
+-- that drives the editor sets FORGE_LIBRARY and supplies its own callbacks instead.
+--
+-- Playing is a scriptPush of the built game. The engine runs this script again from the top when
+-- the game ends, so the description being edited is noted in a file first and reopened on the way
+-- back, which is what makes P feel like a button rather than a restart.
+
+if not FORGE_LIBRARY then
+
+ local LAST_FILE = singeGetDataPath() .. "last.txt" -- What was open when play was pressed.
+ local MANUAL = singeGetDataPath() .. "Forge.pdf" -- The manual, put where a person can open it.
+ local MANUAL_SHOWN = 64 -- Characters of its path the chooser has room for.
+ local ROW_H = 22
+ local LIST_X = 60
+ local LIST_Y = 80
+
+ local chooser = { files = {}, at = 1 }
+ local font = fontLoad("Singe/FreeSansBold.ttf", 15)
+
+ fontSelect(font)
+ fontQuality(FONT_QUALITY_BLENDED)
+ overlaySetResolution(720, 480)
+
+
+ local function chooserScan()
+ chooser.files = forgeFiles()
+ chooser.at = math.max(1, math.min(chooser.at, #chooser.files))
+ end
+
+
+ -- The manual travels inside Forge.game, where nothing but Singe can read it. The first run
+ -- copies it out beside the descriptions, and the chooser says where.
+ local function extractManual()
+ if (lfs.attributes(MANUAL, "mode") ~= "file") and (lfs.attributes("Forge/Forge.pdf", "mode") == "file") then
+ authorCopy("Forge/Forge.pdf", MANUAL)
+ end
+
+ return (lfs.attributes(MANUAL, "mode") == "file") and MANUAL or nil
+ end
+
+
+ local function chooserDraw()
+ local y = LIST_Y
+
+ colorBackground(18, 18, 26, 255)
+ overlayClear()
+ colorForeground(255, 207, 74, 255)
+ fontPrint(LIST_X, 30, "Forge")
+ colorForeground(154, 160, 180, 255)
+ fontPrint(LIST_X + 80, 30, "UP / DOWN choose ENTER open ESC leave")
+ if chooser.manual ~= nil then
+ local shown = chooser.manual
+
+ -- A data directory given on the command line can be anywhere; the end of the path is
+ -- the part that says where the file is.
+ if #shown > MANUAL_SHOWN then
+ shown = "..." .. string.sub(shown, -MANUAL_SHOWN)
+ end
+ fontPrint(LIST_X, overlayGetHeight() - 40, "Manual: " .. shown)
+ end
+ for at, entry in ipairs(chooser.files) do
+ local label = entry.name
+
+ if entry.sample then
+ label = label .. " (a copy will be made)"
+ end
+ if at == chooser.at then
+ colorForeground(192, 60, 150, 255)
+ for row = y - 3, y + ROW_H - 6 do
+ overlayLine(LIST_X - 10, row, LIST_X + 400, row)
+ end
+ colorForeground(255, 255, 255, 255)
+ else
+ colorForeground(230, 230, 238, 255)
+ end
+ fontPrint(LIST_X, y, label)
+ y = y + ROW_H
+ end
+ end
+
+
+ -- Saves, builds, and hands the game to the engine. Back here afterwards, on the same file.
+ function forgePlay()
+ local built = forgeBuild(singeGetDataPath() .. "preview.singe")
+ local note = io.open(LAST_FILE, "w")
+
+ forgeSave()
+ if note ~= nil then
+ note:write(FORGE.path)
+ note:close()
+ end
+ scriptPush({ SCRIPT = built, VIDEO = forgeVideo() })
+ end
+
+
+ -- Where play left off, if it did. The note is read once and removed, so a fresh start shows
+ -- the chooser.
+ local function reopen()
+ local note = io.open(LAST_FILE, "r")
+ local path
+
+ if note == nil then
+ return false
+ end
+ path = note:read("l")
+ note:close()
+ os.remove(LAST_FILE)
+ if (path == nil) or not forgeBegin(path) then
+ return false
+ end
+ FORGE.message = "back from playing"
+ forgeRefresh()
+
+ return true
+ end
+
+
+ local function chooserKey(scancode)
+ if scancode == SCANCODE.DOWN.value then
+ chooser.at = math.min(chooser.at + 1, #chooser.files)
+ elseif scancode == SCANCODE.UP.value then
+ chooser.at = math.max(chooser.at - 1, 1)
+ elseif scancode == SCANCODE.RETURN.value then
+ forgeOpen(chooser.files[chooser.at])
+ elseif scancode == SCANCODE.ESCAPE.value then
+ singeQuit()
+ end
+ end
+
+
+ -- ESC in the editor closes the description; twice, when it has unsaved changes.
+ local function editorEscape()
+ if FORGE.dirty and not FORGE.closing then
+ FORGE.closing = true
+ FORGE.message = "unsaved: S saves, ESC again leaves without saving"
+ forgeRefresh()
+ return
+ end
+ forgeClose()
+ chooserScan()
+ end
+
+
+ function onOverlayUpdate()
+ if FORGE.game == nil then
+ chooserDraw()
+ else
+ local x, y = mouseGetPosition(0)
+
+ forgeDraw(x, y)
+ end
+
+ return OVERLAY_UPDATED
+ end
+
+
+ function onKeyPressed(keysym, scancode)
+ if FORGE.game == nil then
+ chooserKey(scancode)
+ elseif (scancode == SCANCODE.ESCAPE.value) and (FORGE.editing == nil) and (FORGE.picker == nil) then
+ editorEscape()
+ elseif (scancode == SCANCODE.P.value) and (FORGE.editing == nil) and (FORGE.picker == nil) then
+ forgePlay()
+ else
+ forgeKey(keysym, scancode)
+ end
+ end
+
+
+ -- The pad walks the chooser; in the editor the left mouse button is the pointer's press.
+ function onInputPressed(what)
+ if FORGE.game == nil then
+ if what == SWITCH_DOWN then
+ chooserKey(SCANCODE.DOWN.value)
+ elseif what == SWITCH_UP then
+ chooserKey(SCANCODE.UP.value)
+ elseif (what == SWITCH_START1) or (what == SWITCH_BUTTON1) then
+ chooserKey(SCANCODE.RETURN.value)
+ end
+ elseif what == SWITCH_BUTTON3 then
+ local x, y = mouseGetPosition(0)
+
+ forgePress(x, y)
+ end
+ end
+
+
+ function onInputReleased(what)
+ if (FORGE.game ~= nil) and (what == SWITCH_BUTTON3) then
+ forgeRelease()
+ end
+ end
+
+
+ function onMouseMoved(x, y)
+ if FORGE.game ~= nil then
+ forgeDrag(x, y)
+ end
+ end
+
+
+ chooser.manual = extractManual()
+ if not reopen() then
+ chooserScan()
+ end
+
+end
diff --git a/assets/Forge/games.dat b/assets/Forge/games.dat
index eb3c12136..0a701a794 100644
--- a/assets/Forge/games.dat
+++ b/assets/Forge/games.dat
@@ -7,7 +7,6 @@ GAMES = {
SCRIPT = "Forge/Forge.singe",
LEGACY_SPRITE_ARGS = false,
VIDEO = "Singe/menuBackground.mkv",
- DATA = "Forge",
STRETCH = false,
NO_MOUSE = false,
RESOLUTION_X = 720,
diff --git a/assets/Menu.singe b/assets/Menu.singe
index e7184a8f1..11b2c8c4a 100644
--- a/assets/Menu.singe
+++ b/assets/Menu.singe
@@ -81,12 +81,18 @@ end
function loadGameAssets()
local game = GAME_LIST[GAME_SELECTED]
- VIDEO_ATTRACT = videoLoad(game.ATTRACT)
- if game.AUDIO_TRACK then
- videoSetAudioTrack(VIDEO_ATTRACT, game.AUDIO_TRACK)
+ -- An entry without an attract clip, or naming one that is not there, shows its art and no
+ -- video rather than taking the whole menu down with a videoLoad that cannot open it.
+ if menuFileExists(game.ATTRACT) then
+ VIDEO_ATTRACT = videoLoad(game.ATTRACT)
+ if game.AUDIO_TRACK then
+ videoSetAudioTrack(VIDEO_ATTRACT, game.AUDIO_TRACK)
+ end
+ videoPlay(VIDEO_ATTRACT)
+ videoSeek(VIDEO_ATTRACT, game.ATTRACT_START or 0)
+ else
+ debugPrint("Menu: " .. tostring(game.TITLE) .. " has no attract clip to play.")
end
- videoPlay(VIDEO_ATTRACT)
- videoSeek(VIDEO_ATTRACT, game.ATTRACT_START)
MENU_RENDER.showGame(game)
end
@@ -96,18 +102,26 @@ end
MENU_MISSING_ART = "Singe/missing.png"
--- A readable art path: the game's own when it can be opened, the missing-art picture when not.
-function menuArtPath(file)
+-- Whether a name is a file the engine can open. Asked of the index rather than of the file:
+-- io.open, which this used to use, has to hand Lua a real file it can read with the C library, so
+-- every picture it was asked about was unpacked out of the .game into data//cache first --
+-- and the menu asks about two of them for every game in the library, none of which it then opens
+-- that way: the sprite loader and the document's texture loader both read straight out of the
+-- database. lfs.attributes goes through the same virtual filesystem and answers from the index,
+-- opening nothing.
+function menuFileExists(file)
local path = tostring(file or "")
local found = (path ~= "") and lfs.attributes(path) or nil
- -- Asked of the index rather than of the file. io.open, which this used to use, has to hand
- -- Lua a real file it can read with the C library, so every picture it was asked about was
- -- unpacked out of the .game into data//cache first -- and the menu asks about two of
- -- them for every game in the library, none of which it then opens that way: the sprite loader
- -- and the document's texture loader both read straight out of the database. lfs.attributes
- -- goes through the same virtual filesystem and answers from the index, opening nothing.
- if (found == nil) or (found.mode == "directory") then
+ return (found ~= nil) and (found.mode ~= "directory")
+end
+
+
+-- A readable art path: the game's own when it can be opened, the missing-art picture when not.
+function menuArtPath(file)
+ local path = tostring(file or "")
+
+ if not menuFileExists(path) then
if path ~= "" then
debugPrint("Menu: cannot read " .. path .. "; showing the missing-art picture.")
end
@@ -209,8 +223,10 @@ function onOverlayUpdate()
end
-- The attract clip runs between the two frames the game's entry names.
- if videoGetFrame(VIDEO_ATTRACT) > GAME_LIST[GAME_SELECTED].ATTRACT_END then
- videoSeek(VIDEO_ATTRACT, GAME_LIST[GAME_SELECTED].ATTRACT_START)
+ if VIDEO_ATTRACT and GAME_LIST[GAME_SELECTED].ATTRACT_END then
+ if videoGetFrame(VIDEO_ATTRACT) > GAME_LIST[GAME_SELECTED].ATTRACT_END then
+ videoSeek(VIDEO_ATTRACT, GAME_LIST[GAME_SELECTED].ATTRACT_START or 0)
+ end
end
MENU_RENDER.frame()
end
@@ -221,6 +237,10 @@ end
function onShutdown()
+ -- With no games found there is no renderer and nothing was loaded; quitting is all there is.
+ if MENU_RENDER == nil then
+ return
+ end
unloadGameAssets()
saveConfig(not SHUTDOWN_FROM_PUSH)
MENU_RENDER.finish()
@@ -306,7 +326,7 @@ for dir in lfs.dir(".") do
local dirattr = lfs.attributes(dir)
if dir:sub(-5):lower() == ".game" then
loadGamesDat(dir .. "/games.dat", dir)
- elseif dirattr.mode == "directory" then
+ elseif dirattr and (dirattr.mode == "directory") then
for file in lfs.dir(dir .. "/.") do
if file == "games.dat" then
loadGamesDat(dir .. "/games.dat", nil)
diff --git a/assets/MenuDocument.singe b/assets/MenuDocument.singe
index ba4379d32..0b5fc1508 100644
--- a/assets/MenuDocument.singe
+++ b/assets/MenuDocument.singe
@@ -69,8 +69,6 @@ local detailClock = 0
local SCROLL_HOLD = 3.0 -- Seconds it rests at each end before moving, as in the overlay
local SCROLL_STEP = 1.1 -- renderer, and seconds a step while it is moving.
local SCROLL_LINE = 16 -- How far a step goes: a line of the description.
-local backdropRoot = nil
-local backdropGrid = nil
function menuElement(id)
@@ -358,7 +356,9 @@ end
MENU_RENDER.frame = function()
-- Attract Mode Video, under the hole the document leaves for it
- videoDraw(VIDEO_ATTRACT, videoX, videoY, videoX + videoW, videoY + videoH)
+ if VIDEO_ATTRACT then
+ videoDraw(VIDEO_ATTRACT, videoX, videoY, videoX + videoW, videoY + videoH)
+ end
if scrollPending then
menuScrollToSelection()
end
diff --git a/assets/MenuOverlay.singe b/assets/MenuOverlay.singe
index ff0d1288e..f31782ccb 100644
--- a/assets/MenuOverlay.singe
+++ b/assets/MenuOverlay.singe
@@ -430,7 +430,9 @@ MENU_RENDER.frame = function()
spriteDraw(spriteMarquee, x, y)
-- Attract mode video
- videoDraw(VIDEO_ATTRACT, videoX, videoY, videoX + videoW, videoY + videoH)
+ if VIDEO_ATTRACT then
+ videoDraw(VIDEO_ATTRACT, videoX, videoY, videoX + videoW, videoY + videoH)
+ end
-- Which game this is, since there is no list to look at. It heads the details rather than
-- sitting over the cabinet artwork, where it was unreadable against whatever the picture was.
diff --git a/assets/Net.singe b/assets/Net.singe
index a6aca2fb0..39f835144 100644
--- a/assets/Net.singe
+++ b/assets/Net.singe
@@ -39,6 +39,11 @@
--
-- The pin is of the *public key*, not the certificate, so an ordinary renewal that keeps the key
-- does not lock out every cabinet; two pins are carried so a key can actually be rotated.
+--
+-- Without a pin the chain is verified against a CA list. Linux has one; Windows, macOS and the
+-- handhelds have nothing LuaSec can read, so the engine extracts Mozilla's list as
+-- Singe/cacerts.pem beside the other support files and that is used wherever the platform's own
+-- is missing.
local socket = require("socket")
@@ -53,6 +58,9 @@ NET_REDIRECT_MAX = 5
-- host's own trust store is used instead, which is what a self-hosted server without a pin gets.
NET_PINS = {}
+NET_CA_SYSTEM = "/etc/ssl/certs/ca-certificates.crt" -- Linux's list, when it is there,
+NET_CA_SHIPPED = "Singe/cacerts.pem" -- and the one Singe carries otherwise.
+
local active = {}
local nextId = 1
@@ -149,17 +157,28 @@ local function buildRequest(request)
end
+-- The CA list a connection without a pin is verified against.
+local function caFile()
+ local lfs = require("lfs")
+
+ if lfs.attributes(NET_CA_SYSTEM, "mode") == "file" then
+ return NET_CA_SYSTEM
+ end
+
+ return NET_CA_SHIPPED
+end
+
+
local function startTls(request)
local params = {
mode = "client",
protocol = "any",
options = { "all", "no_sslv2", "no_sslv3", "no_tlsv1", "no_tlsv1_1" },
-- With a pin, the chain is checked against the key rather than a CA list, and the pin is
- -- what decides. Without one, the host's own store is asked -- and if it has nothing to
- -- say, the request fails rather than proceeding unverified.
+ -- what decides. Without one, a CA list is asked -- and if it has nothing to say, the
+ -- request fails rather than proceeding unverified.
verify = (#NET_PINS > 0) and "none" or "peer",
- capath = "/etc/ssl/certs",
- cafile = "/etc/ssl/certs/ca-certificates.crt",
+ cafile = caFile(),
}
local wrapped, err = ssl.wrap(request.socket, params)
@@ -238,6 +257,12 @@ local function splitHead(request)
end
request.length = tonumber(request.responseHeaders["content-length"])
request.got = 0
+ -- HTTP/1.1 may send the body in sized pieces instead of announcing a length; those are
+ -- unwrapped as they arrive and the last, empty, piece is the end of the body.
+ request.chunked = (request.responseHeaders["transfer-encoding"] or ""):lower():find("chunked", 1, true) ~= nil
+ request.chunkBuffer = ""
+ request.chunkLeft = nil
+ request.chunkDone = false
if request.toFile then
local file, err = io.open(request.toFile, "wb")
if not file then
@@ -263,6 +288,46 @@ local function consume(request, data)
end
+-- Body bytes arriving under chunked transfer encoding: a hexadecimal size on a line, that many
+-- bytes, a blank line, again until a size of zero. Whatever is left over waits for the next read.
+local function consumeChunked(request, data)
+ request.chunkBuffer = request.chunkBuffer .. data
+ while not request.chunkDone do
+ if request.chunkLeft == nil then
+ local at = request.chunkBuffer:find("\r\n", 1, true)
+ if not at then
+ return
+ end
+ local size = tonumber(request.chunkBuffer:sub(1, at - 1):match("^%x+"), 16)
+ request.chunkBuffer = request.chunkBuffer:sub(at + 2)
+ if size == nil then
+ return fail(request, "the server sent a chunk with no size")
+ end
+ if size == 0 then
+ request.chunkDone = true
+ return
+ end
+ request.chunkLeft = size
+ elseif request.chunkLeft > 0 then
+ local take = request.chunkBuffer:sub(1, request.chunkLeft)
+ if #take == 0 then
+ return
+ end
+ request.chunkBuffer = request.chunkBuffer:sub(#take + 1)
+ request.chunkLeft = request.chunkLeft - #take
+ consume(request, take)
+ else
+ -- The blank line after a chunk's bytes.
+ if #request.chunkBuffer < 2 then
+ return
+ end
+ request.chunkBuffer = request.chunkBuffer:sub(3)
+ request.chunkLeft = nil
+ end
+ end
+end
+
+
local function complete(request)
local body = request.file and "" or table.concat(request.parts)
@@ -277,6 +342,7 @@ local function complete(request)
local _, _, port, path = parseUrl(location)
request.port, request.path = port, path
request.buffer, request.parts, request.outgoing, request.sent = "", {}, nil, 0
+ request.chunked, request.chunkBuffer, request.chunkLeft, request.chunkDone = false, "", nil, false
if request.socket then pcall(function() request.socket:close() end) end
request.socket = nil
return beginConnect(request)
@@ -298,13 +364,22 @@ local function pumpReading(request)
if splitHead(request) and #request.buffer > 0 then
local rest = request.buffer
request.buffer = ""
- consume(request, rest)
+ if request.chunked then
+ consumeChunked(request, rest)
+ else
+ consume(request, rest)
+ end
end
+ elseif request.chunked then
+ consumeChunked(request, got)
else
consume(request, got)
end
end
- if request.responseHeaders and request.length and request.got >= request.length then
+ if not active[request.id] then
+ return -- A bad chunk failed the request from inside consumeChunked.
+ end
+ if request.responseHeaders and ((request.length and request.got >= request.length) or request.chunkDone) then
return complete(request)
end
if err == "closed" then
diff --git a/assets/Tools.singe b/assets/Tools.singe
index dd846b0d7..85734520e 100644
--- a/assets/Tools.singe
+++ b/assets/Tools.singe
@@ -1214,7 +1214,7 @@ end
-- A .game installed here that the catalogue no longer lists. Without this it would vanish from
-- the page the moment it was withdrawn, leaving a player with a game they can play, cannot update,
-- and cannot remove from anywhere but a file manager.
-function shopAddWithdrawn()
+local function shopAddWithdrawn()
local listed = {}
for _, game in ipairs(shopGames) do
diff --git a/assets/cacerts.pem b/assets/cacerts.pem
new file mode 100644
index 000000000..4cde36ec9
--- /dev/null
+++ b/assets/cacerts.pem
@@ -0,0 +1,3004 @@
+# Root certificates the master service is verified against on platforms with no trust store
+# LuaSec can read (Windows, macOS, the handhelds). Mozilla's CA list, as packaged by Debian
+# ca-certificates 20260601, taken 2026-09-13. Distributed under the Mozilla Public License 2.0.
+# Refresh from /etc/ssl/certs/ca-certificates.crt on a current Linux; see Net.singe.
+-----BEGIN CERTIFICATE-----
+MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE
+AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw
+CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ
+BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND
+VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb
+qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY
+HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo
+G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA
+lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr
+IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/
+0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH
+k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47
+4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO
+m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa
+cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl
+uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI
+KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls
+ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG
+AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2
+VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT
+VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG
+CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA
+cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA
+QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA
+7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA
+cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA
+QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA
+czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu
+aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt
+aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud
+DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF
+BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp
+D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU
+JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m
+AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD
+vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms
+tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH
+7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h
+I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA
+h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF
+d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H
+pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx
+CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ
+WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ
+BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG
+Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/
+yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf
+BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz
+WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF
+tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z
+374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC
+IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL
+mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7
+wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS
+MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2
+ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet
+UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw
+AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H
+YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3
+LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD
+nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1
+RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM
+LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf
+77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N
+JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm
+fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp
+6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp
+1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B
+9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok
+RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv
+uu8wd+RU4riEmViAqhOLUTpPSPaLtrM=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw
+CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw
+FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S
+Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5
+MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL
+DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS
+QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB
+BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH
+sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK
+Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
+VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu
+SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC
+MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy
+v+c=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV
+BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk
+YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV
+BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN
+MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF
+UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD
+VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v
+dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj
+cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q
+yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH
+2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX
+H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL
+zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR
+p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz
+W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/
+SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn
+LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3
+n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B
+u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj
+o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO
+BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
+AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L
+9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej
+rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK
+pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0
+vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq
+OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ
+/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9
+2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI
++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2
+MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo
+tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE
+BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w
+MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290
+IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC
+SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1
+ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv
+UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX
+4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9
+KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/
+gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb
+rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ
+51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F
+be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe
+KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F
+v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn
+fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7
+jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz
+ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt
+ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL
+e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70
+jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz
+WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V
+SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j
+pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX
+X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok
+fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R
+K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU
+ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU
+LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT
+LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
+ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6
+b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL
+MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv
+b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj
+ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM
+9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw
+IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6
+VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L
+93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm
+jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA
+A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI
+U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs
+N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv
+o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU
+5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy
+rqXRfboQnoZsG4q5WTP468SQvvG5
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF
+ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6
+b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL
+MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv
+b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK
+gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ
+W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg
+1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K
+8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r
+2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me
+z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR
+8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj
+mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz
+7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6
++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI
+0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB
+Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm
+UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2
+LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY
++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS
+k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl
+7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm
+btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl
+urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+
+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63
+n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE
+76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H
+9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT
+4PsJYGw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5
+MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
+Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
+A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
+Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl
+ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j
+QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr
+ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr
+BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM
+YyRIHN8wfdVoOw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5
+MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
+Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
+A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
+Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi
+9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk
+M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB
+/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB
+MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw
+CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW
+1KyLa2tJElMzrdfkviT8tQp21KW8EA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE
+AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG
+EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM
+FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC
+REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp
+Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM
+VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+
+SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ
+4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L
+cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi
+eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV
+HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG
+A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3
+DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j
+vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP
+DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc
+maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D
+lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv
+KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w
+LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w
+CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0
+MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF
+Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI
+zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X
+tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4
+AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2
+KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD
+aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu
+CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo
+9H1/IISpQuQo
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM
+MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx
+MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00
+MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD
+QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN
+BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z
+4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv
+Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ
+kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs
+GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln
+nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh
+3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD
+0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy
+geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8
+ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB
+c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI
+pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
+dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB
+DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS
+4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs
+o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ
+qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw
+xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM
+rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4
+AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR
+0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY
+o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5
+dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE
+oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE
+BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h
+cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1
+MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg
+Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9
+thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM
+cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG
+L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i
+NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h
+X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b
+m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy
+Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja
+EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T
+KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF
+6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh
+OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc
+tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd
+IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j
+b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC
+AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw
+ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m
+iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF
+Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ
+hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P
+Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE
+EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV
+1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t
+CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR
+5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw
+f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9
+ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK
+GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU
+MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI
+T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz
+MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF
+SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh
+bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z
+xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ
+spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5
+58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR
+at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll
+5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq
+nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK
+V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/
+pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO
+z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn
+jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+
+WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF
+7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE
+AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4
+YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli
+awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u
++2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88
+X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN
+SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo
+P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI
++pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz
+znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9
+eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2
+YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy
+r/6zcCwupvI=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw
+CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ
+VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy
+MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ
+TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS
+b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B
+IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+
++kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK
+sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD
+AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA
+94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B
+43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd
+MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg
+Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow
+TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw
+HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB
+BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr
+6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV
+L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91
+1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx
+MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ
+QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB
+arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr
+Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi
+FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS
+P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN
+9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP
+AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz
+uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h
+9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s
+A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t
+OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo
++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7
+KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2
+DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us
+H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ
+I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7
+5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h
+3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz
+Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd
+MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg
+Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow
+TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw
+HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB
+BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y
+ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E
+N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9
+tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX
+0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c
+/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X
+KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY
+zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS
+O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D
+34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP
+K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3
+AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv
+Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj
+QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV
+cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS
+IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2
+HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa
+O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv
+033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u
+dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE
+kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41
+3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD
+u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq
+4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV
+BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu
+MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy
+MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx
+EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw
+ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe
+NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH
+PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I
+x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe
+QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR
+yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO
+QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912
+H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ
+QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD
+i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs
+nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1
+rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud
+DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI
+hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM
+tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf
+GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb
+lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka
++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal
+TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i
+nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3
+gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr
+G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os
+zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x
+L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD
+TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y
+aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx
+MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j
+aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP
+T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03
+sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL
+TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5
+/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp
+7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz
+EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt
+hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP
+a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot
+aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg
+TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV
+PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv
+cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL
+tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd
+BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB
+ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT
+ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL
+jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS
+ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy
+P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19
+xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d
+Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN
+5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe
+/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z
+AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ
+5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL
+MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
+BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT
+IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw
+MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy
+ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N
+T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR
+FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J
+cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW
+BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/
+BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm
+fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv
+GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB
+hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
+BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5
+MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
+EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
+Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR
+6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X
+pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC
+9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV
+/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf
+Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z
++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w
+qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah
+SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC
+u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf
+Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq
+crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E
+FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB
+/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl
+wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM
+4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV
+2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna
+FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ
+CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK
+boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke
+jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL
+S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb
+QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl
+0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB
+NVOFBkpdn627G190
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw
+CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu
+bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ
+BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s
+eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK
++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2
+QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E
+BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4
+hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm
+ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG
+BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw
+PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy
+dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9
+MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0
+YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2
+1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT
+vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed
+aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0
+1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5
+r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5
+cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ
+wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ
+6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA
+2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH
+Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR
+eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB
+/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u
+d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr
+PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d
+8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi
+1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd
+rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di
+taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7
+lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj
+yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn
+Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy
+yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n
+wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6
+OV+KmalBWQewLK8=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw
+WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw
+MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x
+MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD
+VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX
+BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
+ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO
+ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M
+CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu
+I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm
+TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh
+C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf
+ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz
+IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT
+Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k
+JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5
+hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB
+GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
+FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of
+1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov
+L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo
+dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr
+aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq
+hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L
+6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG
+HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6
+0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB
+lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi
+o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1
+gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v
+faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63
+Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh
+jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw
+3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw
+CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw
+JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT
+EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0
+WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT
+LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX
+BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE
+KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm
+Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj
+QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8
+EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J
+UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn
+nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM
+MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D
+ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU
+cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3
+WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg
+Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw
+IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH
+UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM
+TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU
+BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM
+kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x
+AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV
+HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y
+sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL
+I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8
+J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY
+VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI
+03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB
+gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu
+QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG
+A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz
+OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ
+VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp
+ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3
+b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA
+DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn
+0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB
+OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE
+fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E
+Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m
+o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i
+sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW
+OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez
+Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS
+adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n
+3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD
+AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC
+AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ
+F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf
+CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29
+XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm
+djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/
+WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb
+AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq
+P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko
+b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj
+XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P
+5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi
+DrW5viSP
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6
+MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu
+MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV
+BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw
+MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg
+U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo
+b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ
+n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q
+p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq
+NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF
+8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3
+HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa
+mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi
+7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF
+ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P
+qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ
+v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6
+Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1
+vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD
+ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4
+WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo
+zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR
+5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ
+GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf
+5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq
+0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D
+P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM
+qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP
+0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf
+E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw
+CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS
+VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5
+NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG
+A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB
+BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS
+zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0
+QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/
+VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g
+PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf
+Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l
+dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1
+c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO
+PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW
+wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV
+dWNbFJWcHwHP2NVypw87
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw
+CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS
+VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5
+NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG
+A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB
+BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC
+/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD
+wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3
+OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g
+PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf
+Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l
+dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1
+c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO
+PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA
+y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb
+gfM0agPnIjhQW+0ZT0MW
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF
+MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD
+bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha
+ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM
+HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03
+UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42
+tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R
+ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM
+lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp
+/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G
+A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G
+A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj
+dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy
+MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl
+cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js
+L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL
+BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni
+acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0
+o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K
+zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8
+PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y
+Johw1+qRzT65ysCQblrGXnRl11z+o+I=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF
+MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD
+bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw
+NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV
+BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn
+ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0
+3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z
+qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR
+p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8
+HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw
+ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea
+HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw
+Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh
+c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E
+RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt
+dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku
+Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp
+3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05
+nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF
+CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na
+xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX
+KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv
+b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
+cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA
+n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc
+biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp
+EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA
+bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu
+YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB
+AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW
+BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI
+QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I
+0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni
+lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9
+B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv
+ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo
+IhNzbM8m9Yop5w==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw
+CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
+ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg
+RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV
+UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
+Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq
+hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf
+Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q
+RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
+BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD
+AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY
+JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv
+6pZjamVFkpUBtA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
+MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT
+MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
+b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI
+2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx
+1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ
+q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz
+tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ
+vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP
+BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV
+5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY
+1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4
+NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG
+Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91
+8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe
+pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl
+MrY=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw
+CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
+ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe
+Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw
+EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x
+IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF
+K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG
+fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO
+Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd
+BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx
+AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/
+oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8
+sycX
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw
+CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp
+Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2
+MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
+bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG
+ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS
+7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp
+0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS
+B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49
+BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ
+LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4
+DXZDjC5Ty3zfDBeWUA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN
+MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT
+HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN
+NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs
+IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+
+ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0
+2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp
+wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM
+pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD
+nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po
+sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx
+Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd
+Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX
+KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe
+XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL
+tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv
+TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN
+AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw
+GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H
+PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF
+O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ
+REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik
+AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv
+/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+
+p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw
+MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF
+qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK
+ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg
+RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV
+UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
+Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y
+ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If
+xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV
+ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO
+DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ
+jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/
+CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi
+EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM
+fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY
+uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK
+chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t
+9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD
+ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2
+SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd
++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc
+fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa
+sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N
+cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N
+0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie
+4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI
+r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1
+/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm
+gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC
+VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0
+Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW
+KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl
+cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw
+NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw
+NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy
+ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV
+BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo
+Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4
+4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9
+KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI
+rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi
+94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB
+sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi
+gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo
+kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE
+vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA
+A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t
+O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua
+AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP
+9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/
+eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m
+0vdXcDazv/wor3ElhVsT/h5/WrQ8
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE
+BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ
+IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0
+MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV
+BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w
+HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj
+Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj
+TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u
+KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj
+qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm
+MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12
+ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP
+zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk
+L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC
+jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA
+HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC
+AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB
+/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg
+p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm
+DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5
+COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry
+L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf
+JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg
+IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io
+2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV
+09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ
+XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq
+T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe
+MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw
+CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
+MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
+MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
+Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA
+A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo
+27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w
+Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw
+TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl
+qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH
+szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8
+Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk
+MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92
+wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p
+aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN
+VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID
+AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
+FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb
+C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe
+QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy
+h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4
+7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J
+ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef
+MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/
+Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT
+6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ
+0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm
+2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb
+bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD
+VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG
+A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw
+WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz
+IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
+AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G
+jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2
+4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
+BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7
+VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm
+ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD
+VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG
+A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw
+WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz
+IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
+AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi
+QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR
+HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
+BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D
+9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8
+p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD
+VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh
+bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw
+MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g
+UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT
+BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx
+uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV
+HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/
++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147
+bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk
+MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH
+bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX
+DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD
+QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu
+MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc
+8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke
+hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD
+VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI
+KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg
+515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO
+xwy8p2Fp8fc74SrL+SvzZpA3
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G
+A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp
+Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4
+MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG
+A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8
+RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT
+gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm
+KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd
+QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ
+XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw
+DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o
+LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU
+RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp
+jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK
+6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX
+mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs
+Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH
+WD9f
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg
+MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh
+bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx
+MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET
+MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ
+KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI
+xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k
+ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD
+aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw
+LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw
+1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX
+k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2
+SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h
+bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n
+WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY
+rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce
+MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD
+AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu
+bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN
+nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt
+Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61
+55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj
+vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf
+cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz
+oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp
+nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs
+pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v
+JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R
+8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4
+5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx
+CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD
+ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw
+MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex
+HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA
+IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq
+R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd
+yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud
+DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ
+7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8
++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA
+MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD
+VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy
+MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt
+c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB
+AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ
+OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG
+vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud
+316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo
+0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE
+y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF
+zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE
++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN
+I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs
+x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa
+ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC
+4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
+HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4
+7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg
+JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti
+2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk
+pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF
+FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt
+rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk
+ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5
+u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP
+4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6
+N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3
+vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx
+EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT
+EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp
+ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz
+NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH
+EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE
+AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD
+E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH
+/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy
+DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh
+GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR
+tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA
+AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
+FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX
+WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu
+9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr
+gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo
+2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO
+LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI
+4uJEvlz36hz1
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw
+CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh
+cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v
+dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG
+A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj
+aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg
+Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7
+KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y
+STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw
+AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD
+AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw
+SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN
+nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs
+MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl
+c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg
+Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL
+MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl
+YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv
+b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l
+mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE
+4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv
+a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M
+pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw
+Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b
+LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY
+AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB
+AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq
+E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr
+W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ
+CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF
+MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE
+AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU
+X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3
+f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja
+H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP
+JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P
+zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt
+jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0
+/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT
+BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79
+aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW
+xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU
+63ZTGI0RmLo=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN
+BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl
+c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl
+bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv
+b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ
+BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj
+YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5
+MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0
+dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg
+QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa
+jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC
+MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi
+C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep
+lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof
+TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix
+DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k
+IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT
+N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v
+dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG
+A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh
+ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx
+QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1
+dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA
+4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0
+AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10
+4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C
+ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV
+9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD
+gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6
+Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq
+NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko
+LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc
+Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV
+HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd
+ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I
+XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI
+M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot
+9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V
+Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea
+j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh
+X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ
+l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf
+bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4
+pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK
+e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0
+vm9qp/UsQu0yrbYhnr68
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP
+MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0
+ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa
+Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3
+YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw
+qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv
+Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6
+lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz
+Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ
+KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK
+FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj
+HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr
+y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ
+/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM
+a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6
+fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
+HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG
+SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi
+7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc
+SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza
+ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc
+XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg
+iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho
+L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF
+Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr
+kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+
+vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU
+YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL
+BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ
+SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n
+a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5
+NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT
+CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u
+Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO
+dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI
+VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV
+9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY
+2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY
+vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt
+bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb
+x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+
+l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK
+TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj
+Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP
+BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e
+i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw
+DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG
+7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk
+MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr
+gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk
+GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS
+3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm
+Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+
+l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c
+JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP
+L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa
+LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG
+mpv0
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
+TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
+cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
+WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
+ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
+MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
+h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
+A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
+T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
+B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
+B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
+KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
+OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
+jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
+qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
+rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
+hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
+ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
+3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
+NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
+ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
+TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
+jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
+oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
+4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
+mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
+emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
+CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
+R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
+MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
+ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
+EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
+ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
+AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
+zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
+tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
+/q4AaOeMSQ+2b1tbFfLn
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK
+MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu
+VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw
+MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw
+JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT
+3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU
++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp
+S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1
+bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi
+T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL
+vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK
+Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK
+dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT
+c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv
+l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N
+iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB
+/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD
+ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH
+6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt
+LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93
+nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3
++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK
+W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT
+AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq
+l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG
+4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ
+mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A
+7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN
+MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu
+VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN
+MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0
+MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7
+ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy
+RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS
+bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF
+/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R
+3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw
+EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy
+9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V
+GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ
+2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV
+WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD
+W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/
+BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN
+AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj
+t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV
+DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9
+TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G
+lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW
+mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df
+WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5
++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ
+tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA
+GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv
+8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4
+MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6
+ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD
+VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j
+b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq
+scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO
+xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H
+LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX
+uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD
+yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+
+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q
+rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN
+BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L
+hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB
+QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+
+HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu
+Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg
+QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB
+BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx
+MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA
+A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb
+laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56
+awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo
+JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw
+LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT
+VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk
+LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb
+UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/
+QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+
+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls
+QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD
+VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0
+ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G
+CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y
+OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx
+FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp
+Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o
+dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP
+kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc
+cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U
+fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7
+N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC
+xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1
++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G
+A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM
+Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG
+SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h
+mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk
+ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775
+tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c
+2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t
+HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw
+CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD
+VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw
+MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV
+UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy
+b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq
+hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR
+ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb
+hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E
+BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3
+FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV
+L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB
+iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl
+MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw
+NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
+IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG
+EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N
+aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ
+Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0
+ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1
+HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm
+gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ
+jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc
+aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG
+YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6
+W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K
+UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH
++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q
+W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/
+BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC
+NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC
+LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC
+gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6
+tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh
+SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2
+TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3
+pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR
+xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp
+GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9
+dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN
+AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB
+RA+GsCyRxj3qrg+E
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM
+BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG
+T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0
+aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx
+CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD
+b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB
+dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA
+iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH
+38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE
+HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz
+kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP
+szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq
+vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf
+nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG
+YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo
+0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a
+CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K
+AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I
+36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB
+Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN
+qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj
+cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm
++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL
+hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe
+lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7
+p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8
+piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR
+LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX
+5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO
+dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul
+9XXeifdy
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG
+EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3
+MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl
+cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR
+dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB
+pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM
+b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm
+aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz
+IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT
+lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz
+AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5
+VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG
+ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2
+BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG
+AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M
+U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh
+bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C
++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC
+bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F
+uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2
+XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt
+MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg
+Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i
+YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x
+CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG
+b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh
+bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3
+HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx
+WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX
+1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk
+u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P
+99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r
+M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw
+AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB
+BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh
+cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5
+gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO
+ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf
+aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic
+Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw
+CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91
+bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg
+Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ
+BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu
+ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS
+b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni
+eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W
+p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E
+BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T
+rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV
+57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg
+Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00
+MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV
+wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe
+rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341
+68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh
+4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp
+UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o
+abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc
+3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G
+KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt
+hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO
+Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt
+zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD
+ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC
+MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2
+cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN
+qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5
+YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv
+b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2
+8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k
+NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj
+ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp
+q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt
+nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00
+MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf
+qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW
+n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym
+c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+
+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1
+o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j
+IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq
+IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz
+8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh
+vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l
+7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG
+cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD
+ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66
+AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC
+roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga
+W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n
+lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE
++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV
+csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd
+dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg
+KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM
+HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4
+WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00
+MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR
+/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu
+FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR
+U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c
+ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR
+FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k
+A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw
+eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl
+sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp
+VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q
+A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+
+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD
+ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px
+KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI
+FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv
+oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg
+u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP
+0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf
+3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl
+8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+
+DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN
+PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/
+ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC
+VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T
+U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp
+Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx
+NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv
+dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv
+bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49
+AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA
+VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku
+WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP
+MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX
+5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ
+ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg
+h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV
+BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE
+CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy
+dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy
+MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G
+A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD
+DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq
+M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf
+OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa
+4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9
+HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR
+aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA
+b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ
+Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV
+PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO
+pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu
+UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY
+MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV
+HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4
+9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW
+s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5
+Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg
+cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM
+79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz
+/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt
+ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm
+Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK
+QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ
+w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi
+S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07
+mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC
+VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T
+U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0
+aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz
+WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0
+b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS
+b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB
+BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI
+7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg
+CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud
+EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD
+VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T
+kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+
+gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE
+BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK
+DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp
+Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz
+OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv
+dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv
+bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN
+AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R
+xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX
+qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC
+C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3
+6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh
+/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF
+YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E
+JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc
+US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8
+ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm
++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi
+M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV
+HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G
+A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV
+cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc
+Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs
+PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/
+q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0
+cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr
+a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I
+H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y
+K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu
+nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf
+oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY
+Ic2wBlX7Jz9TkHCpBB5XJ7k=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw
+CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT
+U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2
+MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh
+dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG
+ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm
+acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN
+SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME
+GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW
+uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp
+15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN
+b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO
+MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD
+DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX
+DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw
+b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC
+AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP
+L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY
+t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins
+S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3
+PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO
+L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3
+R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w
+dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS
++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS
+d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG
+AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f
+gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j
+BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z
+NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt
+hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM
+QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf
+R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ
+DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW
+P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy
+lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq
+bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w
+AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q
+r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji
+Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU
+98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL
+BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6
+ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw
+NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L
+cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg
+Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN
+QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT
+3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw
+3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6
+3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5
+BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN
+XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD
+AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF
+AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw
+8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG
+nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP
+oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy
+d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg
+LvWpCz/UXeHPhJ/iGcJfitYgHuNztw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw
+CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T
+ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN
+MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG
+A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT
+ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA
+IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC
+WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+
+6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B
+Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa
+qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q
+4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf
+MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD
+Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw
+HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY
+MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp
+YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB
+AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa
+ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz
+SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf
+iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X
+ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3
+IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS
+VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE
+SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu
++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt
+8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L
+HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt
+zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P
+AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c
+mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ
+YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52
+gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA
+Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB
+JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX
+DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui
+TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5
+dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65
+LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp
+0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY
+QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT
+AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD
+VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx
+NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT
+HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5
+IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
+AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl
+dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK
+ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E
+BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu
+9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O
+be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl
+MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe
+U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX
+DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy
+dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj
+YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV
+OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr
+zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM
+VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ
+hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO
+ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw
+awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs
+OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
+DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF
+coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc
+okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8
+t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy
+1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/
+SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx
+EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT
+HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs
+ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw
+MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6
+b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj
+aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp
+Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg
+nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1
+HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N
+Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN
+dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0
+HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO
+BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G
+CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU
+sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3
+4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg
+8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K
+pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1
+mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx
+EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT
+HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs
+ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5
+MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD
+VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy
+ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy
+dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p
+OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2
+8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K
+Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe
+hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk
+6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw
+DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q
+AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI
+bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB
+ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z
+qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd
+iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn
+0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN
+sSi6
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx
+KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd
+BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl
+YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1
+OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy
+aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50
+ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd
+AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC
+FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi
+1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq
+jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ
+wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj
+QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/
+WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy
+NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC
+uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw
+IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6
+g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN
+9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP
+BSeOE6Fuwg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx
+KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd
+BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl
+YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1
+OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy
+aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50
+ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN
+8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/
+RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4
+hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5
+ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM
+EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj
+QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1
+A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy
+WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ
+1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30
+6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT
+91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml
+e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p
+TpPDpFQUWw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx
+GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp
+bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w
+KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0
+BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy
+dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG
+EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll
+IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU
+QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT
+TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg
+LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7
+a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr
+LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr
+N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X
+YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/
+iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f
+AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH
+V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
+BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh
+AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf
+IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4
+lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c
+8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf
+lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx
+EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT
+VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5
+NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT
+B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF
+10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz
+0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh
+MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH
+zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc
+46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2
+yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi
+laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP
+oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA
+BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE
+qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm
+4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB
+/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL
+1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn
+LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF
+H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo
+RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+
+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh
+15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW
+6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW
+nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j
+wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz
+aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy
+KwbQBM0=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES
+MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU
+V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz
+WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO
+LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm
+aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE
+AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH
+K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX
+RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z
+rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx
+3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq
+hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC
+MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls
+XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D
+lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn
+aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ
+YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx
+CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE
+AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1
+NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ
+MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq
+AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9
+vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9
+lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD
+n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT
+7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o
+6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC
+TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6
+WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R
+DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI
+pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj
+YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy
+rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw
+AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ
+8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi
+0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM
+A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS
+SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K
+TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF
+6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er
+3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt
+Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT
+VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW
+ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA
+rBPuUBQemMc=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM
+BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp
+ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe
+Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw
+IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU
+cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS
+T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK
+AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1
+nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep
+qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA
+yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs
+hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX
+zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv
+kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT
+f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA
+uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB
+o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih
+MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E
+BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4
+wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2
+XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1
+JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j
+ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV
+VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx
+xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on
+AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d
+7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj
+gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV
++Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo
+FGWsJwt0ivKH
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw
+WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs
+IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y
+MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD
+VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz
+dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx
+s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw
+LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij
+YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD
+pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE
+AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR
+UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj
+/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL
+BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg
+Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv
+b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG
+EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u
+IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ
+KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ
+n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd
+2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF
+VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ
+GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF
+li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU
+r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2
+eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb
+MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg
+jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB
+7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW
+5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE
+ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0
+90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z
+xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu
+QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4
+FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH
+22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP
+xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn
+dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5
+Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b
+nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ
+CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH
+u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj
+d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH
+MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF
+eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx
+MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV
+BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB
+AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog
+D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS
+sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop
+O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk
+sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi
+c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj
+VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz
+KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/
+TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G
+sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs
+1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD
+fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T
+AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN
+l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR
+ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ
+VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5
+c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp
+4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s
+t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj
+2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO
+vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C
+xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx
+cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM
+fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9
+MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH
+bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x
+CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds
+b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr
+b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9
+kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm
+VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R
+VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc
+C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj
+tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY
+D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv
+j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl
+NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6
+iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP
+O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/
+BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV
+ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj
+L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5
+1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl
+1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU
+b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV
+PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj
+y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb
+EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg
+DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI
++Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy
+YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX
+UB+K+wb1whnw0A==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL
+MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl
+eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT
+JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx
+MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg
+VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm
+aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo
+I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng
+o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G
+A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD
+VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB
+zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW
+RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB
+iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
+cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
+BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw
+MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU
+aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy
+dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B
+3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY
+tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/
+Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2
+VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT
+79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6
+c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT
+Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l
+c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee
+UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE
+Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd
+BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G
+A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF
+Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO
+VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3
+ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs
+8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR
+iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze
+Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ
+XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/
+qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB
+VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB
+L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG
+jjxDah2nGN59PRbxYvnKkKj9
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV
+BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g
+Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ
+BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ
+R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF
+dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw
+vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ
+uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp
+n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs
+cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW
+xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P
+rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF
+DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx
+DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy
+LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C
+eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB
+/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ
+d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq
+kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC
+b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl
+qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0
+OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c
+NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk
+ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO
+pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj
+03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk
+PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE
+1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX
+QRBdJ3NghVdJIgc=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV
+BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk
+LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv
+b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ
+BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg
+THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v
+IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv
+xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H
+Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G
+A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB
+eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo
+jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ
++efcMQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe
+MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0
+ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe
+Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw
+IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL
+SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH
+SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh
+ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X
+DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1
+TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ
+fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA
+sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU
+WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS
+nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH
+dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip
+NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC
+AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF
+MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH
+ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB
+uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl
+PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP
+JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/
+gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2
+j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6
+5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB
+o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS
+/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z
+Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE
+W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D
+hNQ+IIX3Sj0rnP0qCglN6oH4EZw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG
+EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx
+IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw
+MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln
+biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND
+IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci
+MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti
+sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O
+BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB
+Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c
+3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J
+0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG
+EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo
+bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g
+RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ
+TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s
+b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw
+djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0
+WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS
+fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB
+zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq
+hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB
+CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD
++JbNR6iC8hZVdyR+EhCVBCyj
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG
+A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg
+SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw
+MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln
+biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v
+dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ
+BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ
+HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH
+3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH
+GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c
+xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1
+aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq
+TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
+BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87
+/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4
+kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG
+YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT
++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo
+WXzhriKi4gp6D/piq1JM4fHfyr6DDUI=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD
+VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU
+ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH
+MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO
+MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv
+Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz
+f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO
+8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq
+d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM
+tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt
+Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB
+o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD
+AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x
+PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM
+wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d
+GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH
+6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby
+RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx
+iN66zB+Afko=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw
+RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY
+BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz
+MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u
+LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF
+K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0
+v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd
+e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD
+VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw
+V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA
+AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG
+GJTO
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL
+BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x
+FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx
+MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s
+THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD
+ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc
+IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU
+AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+
+GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9
+8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH
+flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt
+J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim
+0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN
+pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ
+UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW
+OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB
+AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E
+BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet
+8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd
+nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j
+bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM
+Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv
+TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS
+S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr
+I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9
+b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB
+UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P
+Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven
+sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI
+MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE
+LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw
+OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi
+MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN
+AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr
+i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE
+gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8
+k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT
+Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl
+2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U
+cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP
+/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS
+uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+
+0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N
+DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+
+XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61
+GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG
+OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y
+XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI
+FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n
+riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR
+VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc
+LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn
+4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD
+hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG
+koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46
+ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS
+Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80
+knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ
+hJ65bvspmZDogNOfJA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI
+MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE
+LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw
+OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi
+MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN
+AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK
+F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE
+7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe
+EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6
+lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb
+RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV
+jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc
+jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx
+TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+
+ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk
+hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF
+NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH
+kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG
+OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y
+XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14
+QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4
+pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q
+3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU
+t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX
+cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8
+ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT
+2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs
+7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP
+gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst
+Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh
+XBxvWHZks/wCuPWdCg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD
+VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0
+ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU
+TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow
+dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy
+b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T
+emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE
+AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS
+AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v
+SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB
+Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K
+ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI
+zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt
+y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl
+C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6
+uWWL
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw
+CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY
+T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy
+NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp
+b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49
+AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy
+cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N
+2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3
+TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C
+tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR
+QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD
+YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL
+MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE
+AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4
+MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k
+YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ
+KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM
+vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b
+rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk
+ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z
+O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R
+tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS
+jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh
+sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho
+mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu
++zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR
+i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT
+kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
+8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2
+zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33
+I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG
+5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8
+qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP
+AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk
+gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs
+YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1
+9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome
+/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3
+J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2
+wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy
+BiElxky8j3C7DOReIoMt0r7+hVu05L0=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQEL
+BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u
+LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgw
+NTM2NDZaFw00MDA0MDgwNTM2NDZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD
+eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS
+b290IENBMTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6OcE3emhF
+KxS06+QT61d1I02PJC0W6K6OyX2kVzsqdiUzg2zqMoqUm048luT9Ub+ZyZN+v/mt
+p7JIKwccJ/VMvHASd6SFVLX9kHrko+RRWAPNEHl57muTH2SOa2SroxPjcf59q5zd
+J1M3s6oYwlkm7Fsf0uZlfO+TvdhYXAvA42VvPMfKWeP+bl+sg779XSVOKik71gur
+FzJ4pOE+lEa+Ym6b3kaosRbnhW70CEBFEaCeVESE99g2zvVQR9wsMJvuwPWW0v4J
+hscGWa5Pro4RmHvzC1KqYiaqId+OJTN5lxZJjfU+1UefNzFJM3IFTQy2VYzxV4+K
+h9GtxRESOaCtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD
+AgEGMB0GA1UdDgQWBBRXNPN0zwRL1SXm8UC2LEzZLemgrTANBgkqhkiG9w0BAQsF
+AAOCAQEAPrvbFxbS8hQBICw4g0utvsqFepq2m2um4fylOqyttCg6r9cBg0krY6Ld
+mmQOmFxv3Y67ilQiLUoT865AQ9tPkbeGGuwAtEGBpE/6aouIs3YIcipJQMPTw4WJ
+mBClnW8Zt7vPemVV2zfrPIpyMpcemik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA
+8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPSvWKErI4cqc1avTc7bgoitPQV
+55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhgaaaI5gdka9at/
+yOPiZwud9AzqVN/Ssq+xIvEg37xEHA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM
+BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u
+LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw
+NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD
+eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS
+b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/
+FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg
+vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy
+6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo
+/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J
+kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ
+0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib
+y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac
+18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs
+0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB
+SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL
+ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD
+AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk
+86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E
+rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib
+ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT
+zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS
+DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4
+2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo
+FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy
+K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6
+dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl
+Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB
+365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c
+JRNItX+S
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw
+UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM
+dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy
+NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl
+cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290
+IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4
+wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR
+ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB
+Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT
+9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp
+4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6
+bkU6iYAZezKYVWOr62Nuk22rGwlgMU4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL
+BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE
+AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx
+MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT
+d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg
+MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX
+vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7
+LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX
+5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE
+EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt
+/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x
+0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5
+KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM
+0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd
+OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta
+clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK
+wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD
+AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4
+DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL
+BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3
+10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz
+Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ
+iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc
+gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM
+ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF
+LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp
+zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td
+Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0
+rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO
+gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw
+CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH
+bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw
+MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx
+JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE
+AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49
+AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O
+tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP
+f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f
+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
+MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di
+z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn
+27iQ7t0l
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj
+MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0
+eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy
+MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC
+REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG
+A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ
+KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9
+cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV
+cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA
+U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6
+Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug
+BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy
+8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J
+co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg
+8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8
+rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12
+mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg
++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX
+gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2
+p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ
+pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm
+9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw
+M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd
+GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+
+CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t
+xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+
+w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK
+L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj
+X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q
+ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm
+dTdmQRCsu/WU48IxK63nI1bMNSWSs1A=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw
+WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs
+IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw
+NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE
+ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB
+c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/
+AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp
+guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw
+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw
+DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01
+L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR
+OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM
+BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp
+ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN
+MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG
+A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1
+c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
+AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+
+NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ
+Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561
+HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32
+ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb
+xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX
+i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ
+UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j
+TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT
+bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8
+S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA
+MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT
+MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3
+Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4
+iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt
+7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp
+2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ
+g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj
+pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M
+pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP
+XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe
+SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0
+ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy
+323imttUQ/hHWKNddBWcwauwxzQ=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ
+MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290
+IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5
+WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO
+LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg
+Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P
+40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF
+avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/
+34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i
+JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu
+j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf
+Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP
+2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA
+S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA
+oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC
+kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW
+5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD
+VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd
+BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB
+AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t
+tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn
+68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn
+TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t
+RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx
+f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI
+Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz
+8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4
+NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX
+xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6
+t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X
+-----END CERTIFICATE-----
diff --git a/build-docs.sh b/build-docs.sh
index 2e72ca9ab..63eee70fa 100755
--- a/build-docs.sh
+++ b/build-docs.sh
@@ -1,15 +1,19 @@
#!/usr/bin/env bash
-# Build the Singe manual from docs/Manual.adoc.
-# Produces .builddir/Manual.html and .builddir/Manual.pdf. The version comes from CMakeLists.txt.
+# Build the Singe manual from docs/Manual.adoc and the Forge manual from docs/Forge.adoc.
+# Produces .builddir/Manual.html, Manual.pdf, Forge.html and Forge.pdf. The version comes from
+# CMakeLists.txt.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
src=$here/docs/Manual.adoc
+forge=$here/docs/Forge.adoc
-if [[ ! -f $src ]]; then
- echo "error: $src not found" >&2
- exit 1
-fi
+for doc in "$src" "$forge"; do
+ if [[ ! -f $doc ]]; then
+ echo "error: $doc not found" >&2
+ exit 1
+ fi
+done
for tool in asciidoctor asciidoctor-pdf; do
if ! command -v "$tool" > /dev/null; then
echo "error: $tool not found (gem install asciidoctor asciidoctor-pdf rouge)" >&2
@@ -23,6 +27,8 @@ out=$here/.builddir
mkdir -p "$out"
asciidoctor -a revnumber="$version" "$src" -o "$out/Manual.html"
asciidoctor-pdf -a revnumber="$version" "$src" -o "$out/Manual.pdf"
+asciidoctor -a revnumber="$version" "$forge" -o "$out/Forge.html"
+asciidoctor-pdf -a revnumber="$version" "$forge" -o "$out/Forge.pdf"
echo "built:"
-ls -la "$out/Manual.html" "$out/Manual.pdf"
+ls -la "$out/Manual.html" "$out/Manual.pdf" "$out/Forge.html" "$out/Forge.pdf"
diff --git a/cmake/Superbuild.cmake b/cmake/Superbuild.cmake
index 8028ad551..613c05c42 100644
--- a/cmake/Superbuild.cmake
+++ b/cmake/Superbuild.cmake
@@ -457,18 +457,23 @@ ExternalProject_Add(singe
# Forge, packed into .builddir beside the binary so it can be copied out and tested. It is NOT
# part of the engine and nothing of it is embedded; this only puts the distributable where the
# binaries already are. Only a build that produces a runnable binary can pack it, since packing is
-# the engine's own --pack, so cross builds skip it and the host build does the work.
+# the engine's own --pack, so cross builds skip it and the host build does the work. The manual,
+# rendered by the engine's own build into .builddir, goes in beside the scripts: Forge copies it
+# out to its data directory on its first run.
if((KANGAROO_OS STREQUAL "linux") AND (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux"))
file(GLOB forgeSources ${CMAKE_SOURCE_DIR}/assets/Forge/*)
set(forgePack ${CMAKE_SOURCE_DIR}/.builddir/Forge.game)
+ set(forgeManual ${CMAKE_SOURCE_DIR}/.builddir/Forge.pdf)
+ set(forgeStage ${SB_PREFIX}/forgepack)
add_custom_command(
OUTPUT ${forgePack}
# Packing refuses to overwrite, and a stale pack is worse than none.
- COMMAND ${CMAKE_COMMAND} -E rm -f ${forgePack}
- COMMAND ${CMAKE_COMMAND} -E make_directory ${SB_PREFIX}/forgepack
+ COMMAND ${CMAKE_COMMAND} -E rm -rf ${forgePack} ${forgeStage}
+ COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/assets/Forge ${forgeStage}/Forge
+ COMMAND ${CMAKE_COMMAND} -E copy ${forgeManual} ${forgeStage}/Forge/Forge.pdf
# Run it somewhere harmless: the engine writes Singe/ and data/ into the working directory.
- COMMAND ${CMAKE_COMMAND} -E chdir ${SB_PREFIX}/forgepack ${CMAKE_SOURCE_DIR}/.builddir/${singeBinaryName} --pack ${CMAKE_SOURCE_DIR}/assets/Forge ${forgePack}
- DEPENDS singe ${forgeSources}
+ COMMAND ${CMAKE_COMMAND} -E chdir ${forgeStage} ${CMAKE_SOURCE_DIR}/.builddir/${singeBinaryName} --pack ${forgeStage}/Forge ${forgePack}
+ DEPENDS singe ${forgeSources} ${forgeManual}
COMMENT "Packing Forge into .builddir/Forge.game"
VERBATIM
)
diff --git a/docs/Forge.adoc b/docs/Forge.adoc
new file mode 100644
index 000000000..034e7a000
--- /dev/null
+++ b/docs/Forge.adoc
@@ -0,0 +1,253 @@
+= Forge
+Scott Duensing
+:revnumber: 3.00
+:revdate: 2026
+:doctype: book
+:toc: left
+:toclevels: 3
+:sectnums:
+:sectnumlevels: 3
+:source-highlighter: rouge
+:icons: font
+:experimental:
+
+[preface]
+== About Forge
+
+Forge is Singe's authoring tool: a way to make a game by describing it --
+placing things, attaching behaviours, writing rules -- and having ordinary
+Singe Lua written for you. It is itself a Singe game and is distributed on
+its own, beside the engine rather than inside it. This document covers the
+description format, the vocabulary, the compiler and the editor. The engine
+calls the generated code makes (`playerNew`, `collidePointRect`,
+`onKeyPressed`, `scriptPush` and the rest) are documented in the Singe Manual.
+
+== Describing a Game Instead of Writing One
+
+A game can be written as a *description* -- a table of layers, entities and
+rules -- and compiled into an ordinary Singe game.
+`Forge/AuthorCompile.singe` does the compiling and `Author.singe` is the runtime
+the result calls. **No part of Forge ships with Singe** -- not the editor, not
+the compiler, not the runtime -- so a game built with it carries its own copy of
+that runtime and stands entirely on its own. Nothing is interpreted at run time: the rules
+become real Lua `if` statements, so a description costs nothing per frame on a
+Raspberry Pi, and the game it produces can be opened, read and edited by hand
+like any other.
+
+There is no notion of genre anywhere in it. A game declares which of the
+engine's own layers it uses, and that is the only difference between a light
+gun game, a platformer and a quick-time event over video.
+
+=== The three nouns
+
+*Layers* are what the game draws through: `world2d` (physics in the XY plane,
+drawn into the overlay), `overlay` (flat drawing over everything) and `disc`
+(the video the game is played over). A game lists the ones it wants.
+
+*Entities* are things on a layer. Each has a position, a `look` (`box`,
+`sprite` or `text`) and any number of behaviours. Every entity is a node,
+whether or not the 3D scene is drawing, which is what lets a 2D game built this
+way run on a machine with no GPU.
+
+*Behaviours* are bundles over engine calls that already exist: `platformer` is
+the character controller (`playerNew` and friends), `solid` is a
+static body, `drift` moves at a constant velocity. Attaching one is a line in
+the description rather than code.
+
+*Rules* are conditions and actions. Every rule is tested every frame, in the
+order written, and all of a rule's conditions must hold for its actions to run.
+
+.A description, in full
+[source,lua]
+----
+return {
+ title = "One rule",
+ layers = { { kind = "world2d", gravity = 1500 } },
+ entities = {
+ { id = "ground", x = 360, y = 440,
+ look = { kind = "box", w = 720, h = 40, r = 60, g = 70, b = 90 },
+ behaviours = { { kind = "solid" } } },
+ { id = "hero", x = 120, y = 380,
+ look = { kind = "box", w = 24, h = 44, r = 230, g = 90, b = 170 },
+ behaviours = { { kind = "platformer", speed = 210, jump = 620 } } }
+ },
+ rules = {
+ { note = "Run right",
+ when = { { "keyHeld", key = "RIGHT" } },
+ act = { { "run", entity = "hero", direction = 1 } } }
+ }
+}
+----
+
+Compile it and run what comes out:
+
+[source,lua]
+----
+dofile("Forge/AuthorCompile.singe")
+dofile(authorBuild("mygame.game", singeGetDataPath() .. "mygame.singe"))
+----
+
+`testScripts/author/platformer.game` and `testScripts/author/qte.game` are
+worked examples, and `testScripts/scene52.singe` and `scene53.singe` compile
+and play them.
+
+=== The vocabulary, and adding to it
+
+Conditions and actions are not built into the compiler. Each is an entry in
+the `AUTHOR` table declaring its parameters and the Lua it emits, so a new kind
+of game is a set of entries rather than a new release. The conditions today
+are `keyHeld`, `switchHeld`, `timeBetween`, `discBetween`, `onGround`,
+`touching`, `below`, `flagSet` and `once`; the actions are `run`, `jump`,
+`moveTo`, `setText`, `show`, `addScore`, `setFlag`, `discTo` and `lua`.
+
+`once` deserves a word. Rules run every frame, so anything that should happen
+a single time -- a door opening, a score awarded -- needs it:
+
+[source,lua]
+----
+{ when = { { "touching", entity = "hero", other = "prize" },
+ { "once", tag = "prize" } },
+ act = { { "addScore", amount = 100 },
+ { "show", entity = "prize", visible = false } } }
+----
+
+=== The way out
+
+The `lua` action takes a line of Lua and emits it as it stands. It is there on
+purpose: when a rule needs something the vocabulary cannot say, that rule drops
+to Lua and the rest of the game is unaffected. A description is a convenience,
+not a cage, and the compiled output is a normal game you can stop describing
+and start editing whenever it suits you.
+
+=== The editor
+
+`Forge/Forge.singe` edits a description, and it is itself a Singe game. It has
+its own directory beside the games, appears in the menu like one, and packs to
+`Forge.game` with `--pack`; the build does that itself (the `forge` target),
+with this manual inside.
+Nothing in it is a preview: the canvas is the same overlay at the same
+coordinates the game will be played in, so what is placed is what is seen.
+
+Started from the menu it opens on a chooser: the descriptions in its data
+directory, any dropped into the `Forge` directory itself (those are opened as a
+copy, since inside a `.game` they are read only), and *New game*, which writes
+a starter -- ground, a hero that runs and jumps, a score readout -- and opens
+it. The first run copies this manual, `Forge.pdf`, out of `Forge.game` into
+that data directory, and the chooser says where it is. `Esc` in the editor closes the description (twice, when it has unsaved
+changes) and `Esc` on the chooser leaves Forge. `P` plays: the description is
+saved, compiled beside a copy of the runtime, and handed to the engine with
+`scriptPush`; when the game ends Forge comes back on the same file.
+
+A script can drive the editor instead, which is how the test scenes do it:
+
+[source,lua]
+----
+FORGE_LIBRARY = true
+dofile("Forge/Forge.singe")
+forgeBegin("mygame.game")
+
+function onOverlayUpdate()
+ local x, y = mouseGetPosition(0)
+
+ forgeDraw(x, y)
+
+ return OVERLAY_UPDATED
+end
+----
+
+The entity list and the details are an RmlUi document; the canvas beside them is
+drawn into the overlay and picked with `collidePointRect`.
+The two compose because the engine offers a button to the GUI first and passes
+on what it did not use, while pointer motion is never consumed at all -- so
+point `forgePress`, `forgeDrag` and `forgeRelease` at the mouse
+callbacks and clicks on the panels will not reach the canvas.
+
+`forgeSave()` writes the description back, `forgeBuild(path)`
+compiles what is on screen (and puts the runtime beside it, so the result
+plays), and `forgeMove(index, x, y)` moves an entity without a pointer, which
+is how `testScripts/scene54.singe` drives it.
+
+=== Editing the entities
+
+`A` adds a box in the middle of the canvas, `D` duplicates the selected entity
+a little to one side, and `Delete` removes it. `Enter` walks the entity's
+fields the same way it walks a rule's parameters: `id`, `x`, `y`, the look's
+`kind`, then whatever that look takes, then `behaviours` -- typed as a list of
+kinds, `platformer, solid` -- and then each behaviour's own parameters as
+`platformer.speed` and so on. The fields come from the `AUTHOR` manifest, so a
+new look or behaviour is editable the moment it is declared. Renaming an
+entity renames it in every rule that talks about it. A `sprite` look is drawn
+with its image once the file name is right, and as a box until then.
+
+From a script, `forgeEntityAdd(x, y)`, `forgeEntityDuplicate()`,
+`forgeEntityDelete()` and `forgeEntitySet(entity, field, value)` do the same.
+
+=== Editing the rules
+
+The panel shows either the entities or the event sheet; `Tab` swaps them. In
+the rules, the selected rule opens in place and its conditions and actions are
+listed under it, because a rule only means anything whole -- a `when` without a
+`then` tells you nothing.
+
+Point `onKeyPressed` at `forgeKey` and the whole editor
+works without a pointer, which is how the bundled menu has always been driven
+and what a cabinet wants:
+
+[cols="1,4"]
+|===
+| `Tab` | entities or rules
+| Up, Down | move through the list, and through the parts of the open rule
+| Left, Right | slide the panel
+| `Enter` | type a value for whatever is selected; again for its next value
+| `Esc` | put the value back
+| `A`, `D`, `Delete` | add, duplicate, delete the selected entity
+| `N` | a new rule
+| `C`, `T` | add a condition, an action -- picked from the vocabulary, with its help beside it
+| `[`, `]` | move the rule up or down the sheet
+| `Delete` | delete the selected rule, or the selected condition or action
+| `U`, `R` | undo and redo, forty steps deep; a drag or a typed form is one step, and a change by hand ends the redo history
+| `S`, `B`, `P` | save, build, play
+|===
+
+With the rule itself selected rather than one of its parts, `Enter` edits its
+note.
+
+A typed number comes back a number rather than a string, because `100` and
+`"100"` compile to different source and a description that changed shape when a
+value was retyped would stop round-tripping.
+
+From a script, `forgeRuleNew(note)`, `forgeRuleAdd("when"|"act",
+name)`, `forgePartSet(key, value)`, `forgePartDelete()`, `forgeRuleMove(by)`,
+`forgeUndo()` and `forgeRedo()` do the same work. Adding a condition selects it, so its
+parameters can be set at once.
+
+The vocabulary comes from the `AUTHOR` manifest, so the rule editor never needs
+changing when a condition or an action is added: it offers whatever is
+declared. A new parameter gets a sensible starting value for its type, so a
+rule compiles the moment it is made rather than only once every field is
+filled in.
+
+A description survives the round trip: loading one, saving it and loading it
+again compiles to the same game, byte for byte. The editor depends on that and
+the test asserts it.
+
+=== Releasing a game
+
+`forgeExport(folder, name)` writes everything a finished game needs into a
+directory of its own: the compiled script, a `games.dat` so the menu lists it,
+the description it was built from so it can be opened again, and **a copy of
+the runtime**, taken out of Forge. `--pack` turns that directory into a `.game`
+like any other, and it runs on a machine that has never had Forge on it.
+
+Every built game finds its own directory to load that runtime from, using
+`debug.getinfo` rather than `DIR`: `DIR` is the directory of the script the
+engine was *launched* with, so a game reached by `dofile` -- a test, a
+launcher, a preview -- would otherwise look beside the caller.
+
+The panel slides. Drag the tab on its outer edge and it moves across the
+window, so an entity that lives underneath it -- a score readout at `12, 12`
+does -- is never permanently out of reach. `forgePanelTo(x)` moves it from
+a script, and `forgeOverPanel(x)` says whether a point is currently
+covered.
+
+[#migrating]
diff --git a/docs/Manual.adoc b/docs/Manual.adoc
index 2eae26bd0..29104e5b2 100644
--- a/docs/Manual.adoc
+++ b/docs/Manual.adoc
@@ -1421,7 +1421,6 @@ GAMES = {
TITLE = ".38 Ambush Alley",
SCRIPT = "ActionMax/38AmbushAlley.singe",
VIDEO = "ActionMax/frame_38AmbushAlley.txt",
- DATA = "ActionMax",
STRETCH = false,
NO_MOUSE = false,
RESOLUTION_X = 720,
@@ -3192,195 +3191,12 @@ die.
=== Describing a Game Instead of Writing One
A game can be written as a *description* -- a table of layers, entities and
-rules -- and compiled into an ordinary Singe game.
-`Forge/AuthorCompile.singe` does the compiling and `Author.singe` is the runtime
-the result calls. **No part of Forge ships with Singe** -- not the editor, not
-the compiler, not the runtime -- so a game built with it carries its own copy of
-that runtime and stands entirely on its own. Nothing is interpreted at run time: the rules
-become real Lua `if` statements, so a description costs nothing per frame on a
-Raspberry Pi, and the game it produces can be opened, read and edited by hand
-like any other.
+rules -- and compiled into an ordinary Singe game by Forge, the authoring
+tool. Forge is distributed on its own, beside the engine, and has its own
+manual: `docs/Forge.adoc` in the source tree, built to `Forge.html` and
+`Forge.pdf` beside this one.
-There is no notion of genre anywhere in it. A game declares which of the
-engine's own layers it uses, and that is the only difference between a light
-gun game, a platformer and a quick-time event over video.
-==== The three nouns
-
-*Layers* are what the game draws through: `world2d` (physics in the XY plane,
-drawn into the overlay), `overlay` (flat drawing over everything) and `disc`
-(the video the game is played over). A game lists the ones it wants.
-
-*Entities* are things on a layer. Each has a position, a `look` (`box`,
-`sprite` or `text`) and any number of behaviours. Every entity is a node,
-whether or not the 3D scene is drawing, which is what lets a 2D game built this
-way run on a machine with no GPU.
-
-*Behaviours* are bundles over engine calls that already exist: `platformer` is
-the character controller (<> and friends), `solid` is a
-static body, `drift` moves at a constant velocity. Attaching one is a line in
-the description rather than code.
-
-*Rules* are conditions and actions. Every rule is tested every frame, in the
-order written, and all of a rule's conditions must hold for its actions to run.
-
-.A description, in full
-[source,lua]
-----
-return {
- title = "One rule",
- layers = { { kind = "world2d", gravity = 1500 } },
- entities = {
- { id = "ground", x = 360, y = 440,
- look = { kind = "box", w = 720, h = 40, r = 60, g = 70, b = 90 },
- behaviours = { { kind = "solid" } } },
- { id = "hero", x = 120, y = 380,
- look = { kind = "box", w = 24, h = 44, r = 230, g = 90, b = 170 },
- behaviours = { { kind = "platformer", speed = 210, jump = 620 } } }
- },
- rules = {
- { note = "Run right",
- when = { { "keyHeld", key = "RIGHT" } },
- act = { { "run", entity = "hero", direction = 1 } } }
- }
-}
-----
-
-Compile it and run what comes out:
-
-[source,lua]
-----
-dofile("Forge/AuthorCompile.singe")
-dofile(authorBuild("mygame.game", singeGetDataPath() .. "mygame.singe"))
-----
-
-`testScripts/author/platformer.game` and `testScripts/author/qte.game` are
-worked examples, and `testScripts/scene52.singe` and `scene53.singe` compile
-and play them.
-
-==== The vocabulary, and adding to it
-
-Conditions and actions are not built into the compiler. Each is an entry in
-the `AUTHOR` table declaring its parameters and the Lua it emits, so a new kind
-of game is a set of entries rather than a new release. The conditions today
-are `keyHeld`, `switchHeld`, `timeBetween`, `discBetween`, `onGround`,
-`touching`, `below`, `flagSet` and `once`; the actions are `run`, `jump`,
-`moveTo`, `setText`, `show`, `addScore`, `setFlag`, `discTo` and `lua`.
-
-`once` deserves a word. Rules run every frame, so anything that should happen
-a single time -- a door opening, a score awarded -- needs it:
-
-[source,lua]
-----
-{ when = { { "touching", entity = "hero", other = "prize" },
- { "once", tag = "prize" } },
- act = { { "addScore", amount = 100 },
- { "show", entity = "prize", visible = false } } }
-----
-
-==== The way out
-
-The `lua` action takes a line of Lua and emits it as it stands. It is there on
-purpose: when a rule needs something the vocabulary cannot say, that rule drops
-to Lua and the rest of the game is unaffected. A description is a convenience,
-not a cage, and the compiled output is a normal game you can stop describing
-and start editing whenever it suits you.
-
-==== The editor
-
-`Forge/Forge.singe` edits a description, and it is itself a Singe game. It has
-its own directory beside the games, appears in the menu like one, and packs to
-`Forge.game` with `--pack`.
-Nothing in it is a preview: the canvas is the same overlay at the same
-coordinates the game will be played in, so what is placed is what is seen.
-
-[source,lua]
-----
-FORGE_LIBRARY = true
-dofile("Forge/Forge.singe")
-forgeBegin("mygame.game")
-
-function onOverlayUpdate()
- local x, y = mouseGetPosition(0)
-
- forgeDraw(x, y)
-
- return OVERLAY_UPDATED
-end
-----
-
-The entity list and the details are an RmlUi document; the canvas beside them is
-drawn into the overlay and picked with <>.
-The two compose because the engine offers a button to the GUI first and passes
-on what it did not use, while pointer motion is never consumed at all -- so
-point `forgePress`, `forgeDrag` and `forgeRelease` at the mouse
-callbacks and clicks on the panels will not reach the canvas.
-
-`forgeSave()` writes the description back, `forgeBuild(path)`
-compiles what is on screen, and `forgeMove(index, x, y)` moves an entity
-without a pointer, which is how `testScripts/scene54.singe` drives it.
-
-==== Editing the rules
-
-The panel shows either the entities or the event sheet; `Tab` swaps them. In
-the rules, the selected rule opens in place and its conditions and actions are
-listed under it, because a rule only means anything whole -- a `when` without a
-`then` tells you nothing.
-
-Point <> at `forgeKey` and the whole editor
-works without a pointer, which is how the bundled menu has always been driven
-and what a cabinet wants:
-
-[cols="1,4"]
-|===
-| `Tab` | entities or rules
-| Up, Down | move through the list, and through the parts of the open rule
-| Left, Right | slide the panel
-| `Enter` | type a value for the selected condition or action; again for its next value
-| `Esc` | put the value back
-| `N` | a new rule
-| `Delete` | delete the selected rule, or the selected condition or action
-| `S`, `B` | save, build
-|===
-
-A typed number comes back a number rather than a string, because `100` and
-`"100"` compile to different source and a description that changed shape when a
-value was retyped would stop round-tripping.
-
-From a script, `forgeRuleNew(note)`, `forgeRuleAdd("when"|"act",
-name)`, `forgePartSet(key, value)` and `forgePartDelete()` do the
-same work. Adding a condition selects it, so its parameters can be set at once.
-
-The vocabulary comes from the `AUTHOR` manifest, so the rule editor never needs
-changing when a condition or an action is added: it offers whatever is
-declared. A new parameter gets a sensible starting value for its type, so a
-rule compiles the moment it is made rather than only once every field is
-filled in.
-
-A description survives the round trip: loading one, saving it and loading it
-again compiles to the same game, byte for byte. The editor depends on that and
-the test asserts it.
-
-==== Releasing a game
-
-`forgeExport(folder, name)` writes everything a finished game needs into a
-directory of its own: the compiled script, a `games.dat` so the menu lists it,
-the description it was built from so it can be opened again, and **a copy of
-the runtime**, taken out of Forge. `--pack` turns that directory into a `.game`
-like any other, and it runs on a machine that has never had Forge on it.
-
-Every built game finds its own directory to load that runtime from, using
-`debug.getinfo` rather than `DIR`: `DIR` is the directory of the script the
-engine was *launched* with, so a game reached by `dofile` -- a test, a
-launcher, a preview -- would otherwise look beside the caller.
-
-The panel slides. Drag the tab on its outer edge and it moves across the
-window, so an entity that lives underneath it -- a score readout at `12, 12`
-does -- is never permanently out of reach. `forgePanelTo(x)` moves it from
-a script, and `forgeOverPanel(x)` says whether a point is currently
-covered.
-
-[#migrating]
=== Migrating from Singe 2.10
Singe 3.00 moved the sprite handle to the first argument of `spriteDraw`,
diff --git a/src/decode.c b/src/decode.c
index 9a2810f0a..997d28d92 100644
--- a/src/decode.c
+++ b/src/decode.c
@@ -477,30 +477,6 @@ SDL_Surface *decodeImage(const void *bytes, size_t size) {
}
-SDL_Surface *decodeImageIO(SDL_IOStream *io, bool closeio) {
- SDL_Surface *surface = IMG_Load_IO(io, false);
- void *bytes = NULL;
- size_t size = 0;
-
- if (surface == NULL) {
- // SDL_image could not read it. It may still be a TIFF, an OpenEXR, a JPEG 2000, an AVIF or
- // any of the other pictures the bundled FFmpeg decodes for video.
- if (SDL_SeekIO(io, 0, SDL_IO_SEEK_SET) >= 0) {
- bytes = SDL_LoadFile_IO(io, &size, false);
- if (bytes != NULL) {
- surface = decodeImage(bytes, size);
- SDL_free(bytes);
- }
- }
- }
- if (closeio) {
- SDL_CloseIO(io);
- }
-
- return surface;
-}
-
-
float *decodeImageFloat(const void *bytes, size_t size, int32_t *width, int32_t *height) {
const AVPixFmtDescriptor *description = NULL;
SourceT source;
@@ -576,3 +552,28 @@ float *decodeImageFloat(const void *bytes, size_t size, int32_t *width, int32_t
return out;
}
+
+
+SDL_Surface *decodeImageIO(SDL_IOStream *io, bool closeio) {
+ SDL_Surface *surface = IMG_Load_IO(io, false);
+ void *bytes = NULL;
+ size_t size = 0;
+
+ if (surface == NULL) {
+ // SDL_image could not read it. It may still be a TIFF, an OpenEXR, a JPEG 2000, an AVIF or
+ // any of the other pictures the bundled FFmpeg decodes for video.
+ if (SDL_SeekIO(io, 0, SDL_IO_SEEK_SET) >= 0) {
+ bytes = SDL_LoadFile_IO(io, &size, false);
+ if (bytes != NULL) {
+ surface = decodeImage(bytes, size);
+ SDL_free(bytes);
+ }
+ }
+ }
+ if (closeio) {
+ SDL_CloseIO(io);
+ }
+
+ return surface;
+}
+
diff --git a/src/embedded.h b/src/embedded.h
index aaa69a30f..7318e4e3a 100644
--- a/src/embedded.h
+++ b/src/embedded.h
@@ -36,6 +36,7 @@
#include "generated/Menu_singe.h"
#include "generated/Tools_singe.h"
#include "generated/Net_singe.h"
+#include "generated/cacerts_pem.h"
#include "generated/Master_singe.h"
#include "generated/Backdrop_singe.h"
#include "generated/MenuDocument_singe.h"
diff --git a/src/guiRender.cpp b/src/guiRender.cpp
index fb3438245..c2cb5f86b 100644
--- a/src/guiRender.cpp
+++ b/src/guiRender.cpp
@@ -819,9 +819,11 @@ Rml::TextureHandle GuiRenderT::LoadTexture(Rml::Vector2i &textureDimensions, con
SDL_Surface *surface = nullptr;
SDL_Surface *converted = nullptr;
Rml::byte *pixels = nullptr;
+ Rml::byte *row = nullptr;
size_t fileSize = 0;
- size_t pixelSize = 0;
- size_t i = 0;
+ size_t rowSize = 0;
+ size_t x = 0;
+ size_t y = 0;
size_t j = 0;
size_t dot = source.rfind('.');
Rml::String extension = (dot == Rml::String::npos) ? Rml::String() : source.substr(dot + 1);
@@ -865,16 +867,23 @@ Rml::TextureHandle GuiRenderT::LoadTexture(Rml::Vector2i &textureDimensions, con
}
surface = converted;
}
- // Premultiplied alpha, which compositing needs.
- pixelSize = (size_t)surface->w * (size_t)surface->h * BYTES_PER_PIXEL;
- pixels = static_cast(surface->pixels);
- for (i = 0; i < pixelSize; i += BYTES_PER_PIXEL) {
- for (j = 0; j < COLOUR_CHANNELS; j++) {
- pixels[i + j] = (Rml::byte)((int)pixels[i + j] * (int)pixels[i + COLOUR_CHANNELS] / COLOUR_MAX);
+ // Premultiplied alpha, which compositing needs. Rows are packed as they go, since the surface
+ // may pad its pitch and GenerateTexture wants the rows back to back.
+ rowSize = (size_t)surface->w * BYTES_PER_PIXEL;
+ pixels = static_cast(surface->pixels);
+ for (y = 0; y < (size_t)surface->h; y++) {
+ row = pixels + (y * (size_t)surface->pitch);
+ for (x = 0; x < rowSize; x += BYTES_PER_PIXEL) {
+ for (j = 0; j < COLOUR_CHANNELS; j++) {
+ row[x + j] = (Rml::byte)((int)row[x + j] * (int)row[x + COLOUR_CHANNELS] / COLOUR_MAX);
+ }
+ }
+ if (row != pixels + (y * rowSize)) {
+ memmove(pixels + (y * rowSize), row, rowSize);
}
}
textureDimensions = { surface->w, surface->h };
- handle = GenerateTexture(Rml::Span(pixels, (size_t)surface->pitch * (size_t)surface->h), textureDimensions);
+ handle = GenerateTexture(Rml::Span(pixels, rowSize * (size_t)surface->h), textureDimensions);
SDL_DestroySurface(surface);
return handle;
}
diff --git a/src/main.c b/src/main.c
index 3425a9706..198d01ddf 100644
--- a/src/main.c
+++ b/src/main.c
@@ -287,8 +287,8 @@ static int32_t _optionIndex(int32_t code);
static int32_t _optionNamed(const char *name);
static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]);
static bool _parseBoolean(const char *text, bool *value);
-static bool _parseGamepadOrder(const char *text);
static bool _parseFloat(const char *text, double *value);
+static bool _parseGamepadOrder(const char *text);
static bool _parseInteger(const char *text, int32_t *value);
static void _requireRange(const char *exeName, const char *source, int32_t value, int32_t min, int32_t max, const char *what, const char *unit);
static void _requireRangeFloat(const char *exeName, const char *source, double value, double min, double max, const char *what);
@@ -950,98 +950,6 @@ static void _crashHandler(int signalNumber) {
#endif
-// Which audio formats this build can decode, for a bug report and so a user whose music is silent
-// can see at a glance whether its format was ever compiled in. The mixer keeps the list.
-char *mainDescribeAudioDecoders(void) {
- char *list = strdup("");
- char *grown = NULL;
- int32_t count = MIX_GetNumAudioDecoders();
- int32_t x = 0;
-
- for (x = 0; x < count; x++) {
- grown = utilCreateString("%s%s%s", list, (x > 0) ? ", " : "", MIX_GetAudioDecoder(x));
- free(list);
- list = grown;
- }
-
- return list;
-}
-
-
-// The processor, for a bug report. Every platform keeps the name somewhere different, and none of
-// them is worth failing over: the core count is always there as a fallback.
-char *mainDescribeCpu(void) {
-#ifdef _WIN32
- char name[128];
- DWORD bytes = sizeof(name);
-
- if (RegGetValueA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", "ProcessorNameString", RRF_RT_REG_SZ, NULL, name, &bytes) == ERROR_SUCCESS) {
- return utilCreateString("%s, %d cores, %d MB", name, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
- }
-#elif defined(__APPLE__)
- char name[128];
- size_t bytes = sizeof(name);
-
- if (sysctlbyname("machdep.cpu.brand_string", name, &bytes, NULL, 0) == 0) {
- return utilCreateString("%s, %d cores, %d MB", name, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
- }
-#else
- char line[256];
- FILE *info = fopen("/proc/cpuinfo", "r");
- char *colon = NULL;
- char *end = NULL;
- char *found = NULL;
-
- // /proc/cpuinfo calls it "model name" on x86 and "Model" on a Raspberry Pi. The kernel
- // reports its size as zero, so it is read a line at a time rather than in one piece.
- while ((info != NULL) && (found == NULL) && (fgets(line, sizeof(line), info) != NULL)) {
- if (utilStartsWith(line, "model name") || utilStartsWith(line, "Model")) {
- colon = strchr(line, ':');
- if (colon != NULL) {
- colon++;
- while (*colon == ' ') {
- colon++;
- }
- for (end = colon + strlen(colon); (end > colon) && ((uint8_t)end[-1] <= ' '); end--) {
- end[-1] = 0;
- }
- found = utilCreateString("%s, %d cores, %d MB", colon, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
- }
- }
- }
- if (info != NULL) {
- fclose(info);
- }
- if (found != NULL) {
- return found;
- }
-#endif
-
- return utilCreateString("%s, %d cores, %d MB", SDL_GetPlatform(), SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
-}
-
-
-// The operating system and its version, for a bug report.
-char *mainDescribeOs(void) {
-#ifdef _WIN32
- char release[64];
- DWORD bytes = sizeof(release);
-
- if (RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "CurrentBuild", RRF_RT_REG_SZ, NULL, release, &bytes) == ERROR_SUCCESS) {
- return utilCreateString("%s build %s", SDL_GetPlatform(), release);
- }
-#else
- struct utsname system;
-
- if (uname(&system) == 0) {
- return utilCreateString("%s %s (%s)", system.sysname, system.release, system.machine);
- }
-#endif
-
- return strdup(SDL_GetPlatform());
-}
-
-
// Writes an embedded support file, or rewrites it when the installed copy differs from this build's.
static bool _extractFile(const char *filename, const uint8_t *data, size_t length) {
FILE *out = NULL;
@@ -1428,33 +1336,6 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
}
-// --gamepad_reorder is a list of enumeration positions, one per gamepad slot, written as Hypseus
-// writes it: bare digits (3210) or separated by commas or spaces (3, 2, 1, 0). A repeated position
-// is the user contradicting themselves and is refused; anything but a digit or a separator is too.
-static bool _parseGamepadOrder(const char *text) {
- bool seen[GAMEPAD_ORDER_DIGITS];
- int32_t digits = 0;
- int32_t x = 0;
-
- memset(seen, 0, sizeof(seen));
- for (x = 0; text[x] != '\0'; x++) {
- if ((text[x] == ',') || (text[x] == ' ')) {
- continue;
- }
- if ((text[x] < '0') || (text[x] > '9')) {
- return false;
- }
- if (seen[text[x] - '0']) {
- return false;
- }
- seen[text[x] - '0'] = true;
- digits++;
- }
-
- return (digits > 0);
-}
-
-
// The words a settings file (and --linearscale) may use for a switch.
static bool _parseBoolean(const char *text, bool *value) {
static const char *yes[] = { "true", "yes", "on", "1" };
@@ -1494,6 +1375,33 @@ static bool _parseFloat(const char *text, double *value) {
}
+// --gamepad_reorder is a list of enumeration positions, one per gamepad slot, written as Hypseus
+// writes it: bare digits (3210) or separated by commas or spaces (3, 2, 1, 0). A repeated position
+// is the user contradicting themselves and is refused; anything but a digit or a separator is too.
+static bool _parseGamepadOrder(const char *text) {
+ bool seen[GAMEPAD_ORDER_DIGITS];
+ int32_t digits = 0;
+ int32_t x = 0;
+
+ memset(seen, 0, sizeof(seen));
+ for (x = 0; text[x] != '\0'; x++) {
+ if ((text[x] == ',') || (text[x] == ' ')) {
+ continue;
+ }
+ if ((text[x] < '0') || (text[x] > '9')) {
+ return false;
+ }
+ if (seen[text[x] - '0']) {
+ return false;
+ }
+ seen[text[x] - '0'] = true;
+ digits++;
+ }
+
+ return (digits > 0);
+}
+
+
static bool _parseInteger(const char *text, int32_t *value) {
char *end = NULL;
long parsed = 0;
@@ -1764,6 +1672,29 @@ static void _stopSDL(void) {
}
+// Reduces every component that names a packed game to its stem, in place. A game writes to one
+// data directory whether it was reached as the container the engine opened ("ActionMax.game", which
+// createDataDirFor already reduces) or by a name that passes through it ("ActionMax.game/video.mkv",
+// which is how a script names a file inside one). Without this the two disagreed: a packed game's
+// video index went to data/ActionMax.game while everything else it wrote went to data/ActionMax,
+// and the library appeared to have two of every game.
+static void _stripDatabaseNames(char *path) {
+ const size_t extension = strlen(VFS_DATABASE_EXTENSION);
+ size_t i = 0;
+ char next = 0;
+
+ for (i = 0; path[i] != 0; i++) {
+ if (strncasecmp(path + i, VFS_DATABASE_EXTENSION, extension) != 0) {
+ continue;
+ }
+ next = path[i + extension];
+ if ((next == 0) || (next == '/') || (next == '\\')) {
+ memmove(path + i, path + i + extension, strlen(path + i + extension) + 1);
+ }
+ }
+}
+
+
// What a bug report needs, at the top of trace.txt and nowhere else: the build, the command that
// started it, the machine, and what the engine picked to run on. main.c's own trace lines wait
// behind it so the block is always first and can be pasted whole.
@@ -1810,6 +1741,7 @@ static void _unpackData(const char *exePath, bool absolute) {
{ "Menu.singe", Menu_singe, Menu_singe_len },
{ "Tools.singe", Tools_singe, Tools_singe_len },
{ "Net.singe", Net_singe, Net_singe_len },
+ { "cacerts.pem", cacerts_pem, cacerts_pem_len },
{ "Master.singe", Master_singe, Master_singe_len },
{ "Backdrop.singe", Backdrop_singe, Backdrop_singe_len },
{ "MenuDocument.singe", MenuDocument_singe, MenuDocument_singe_len },
@@ -1909,29 +1841,6 @@ ConfigT *cloneConf(const ConfigT *conf) {
}
-// Reduces every component that names a packed game to its stem, in place. A game writes to one
-// data directory whether it was reached as the container the engine opened ("ActionMax.game", which
-// createDataDirFor already reduces) or by a name that passes through it ("ActionMax.game/video.mkv",
-// which is how a script names a file inside one). Without this the two disagreed: a packed game's
-// video index went to data/ActionMax.game while everything else it wrote went to data/ActionMax,
-// and the library appeared to have two of every game.
-static void _stripDatabaseNames(char *path) {
- const size_t extension = strlen(VFS_DATABASE_EXTENSION);
- size_t i = 0;
- char next = 0;
-
- for (i = 0; path[i] != 0; i++) {
- if (strncasecmp(path + i, VFS_DATABASE_EXTENSION, extension) != 0) {
- continue;
- }
- next = path[i + extension];
- if ((next == 0) || (next == '/') || (next == '\\')) {
- memmove(path + i, path + i + extension, strlen(path + i + extension) + 1);
- }
- }
-}
-
-
// Builds and creates dataDirBase + directory of filename. Returns a new string or NULL on failure.
char *createDataDir(const char *dataDirBase, const char *filename) {
const char separator = utilGetPathSeparator();
@@ -2020,6 +1929,98 @@ bool isFrameFileName(const char *filename) {
}
+// Which audio formats this build can decode, for a bug report and so a user whose music is silent
+// can see at a glance whether its format was ever compiled in. The mixer keeps the list.
+char *mainDescribeAudioDecoders(void) {
+ char *list = strdup("");
+ char *grown = NULL;
+ int32_t count = MIX_GetNumAudioDecoders();
+ int32_t x = 0;
+
+ for (x = 0; x < count; x++) {
+ grown = utilCreateString("%s%s%s", list, (x > 0) ? ", " : "", MIX_GetAudioDecoder(x));
+ free(list);
+ list = grown;
+ }
+
+ return list;
+}
+
+
+// The processor, for a bug report. Every platform keeps the name somewhere different, and none of
+// them is worth failing over: the core count is always there as a fallback.
+char *mainDescribeCpu(void) {
+#ifdef _WIN32
+ char name[128];
+ DWORD bytes = sizeof(name);
+
+ if (RegGetValueA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", "ProcessorNameString", RRF_RT_REG_SZ, NULL, name, &bytes) == ERROR_SUCCESS) {
+ return utilCreateString("%s, %d cores, %d MB", name, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
+ }
+#elif defined(__APPLE__)
+ char name[128];
+ size_t bytes = sizeof(name);
+
+ if (sysctlbyname("machdep.cpu.brand_string", name, &bytes, NULL, 0) == 0) {
+ return utilCreateString("%s, %d cores, %d MB", name, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
+ }
+#else
+ char line[256];
+ FILE *info = fopen("/proc/cpuinfo", "r");
+ char *colon = NULL;
+ char *end = NULL;
+ char *found = NULL;
+
+ // /proc/cpuinfo calls it "model name" on x86 and "Model" on a Raspberry Pi. The kernel
+ // reports its size as zero, so it is read a line at a time rather than in one piece.
+ while ((info != NULL) && (found == NULL) && (fgets(line, sizeof(line), info) != NULL)) {
+ if (utilStartsWith(line, "model name") || utilStartsWith(line, "Model")) {
+ colon = strchr(line, ':');
+ if (colon != NULL) {
+ colon++;
+ while (*colon == ' ') {
+ colon++;
+ }
+ for (end = colon + strlen(colon); (end > colon) && ((uint8_t)end[-1] <= ' '); end--) {
+ end[-1] = 0;
+ }
+ found = utilCreateString("%s, %d cores, %d MB", colon, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
+ }
+ }
+ }
+ if (info != NULL) {
+ fclose(info);
+ }
+ if (found != NULL) {
+ return found;
+ }
+#endif
+
+ return utilCreateString("%s, %d cores, %d MB", SDL_GetPlatform(), SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
+}
+
+
+// The operating system and its version, for a bug report.
+char *mainDescribeOs(void) {
+#ifdef _WIN32
+ char release[64];
+ DWORD bytes = sizeof(release);
+
+ if (RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "CurrentBuild", RRF_RT_REG_SZ, NULL, release, &bytes) == ERROR_SUCCESS) {
+ return utilCreateString("%s build %s", SDL_GetPlatform(), release);
+ }
+#else
+ struct utsname system;
+
+ if (uname(&system) == 0) {
+ return utilCreateString("%s %s (%s)", system.sysname, system.release, system.machine);
+ }
+#endif
+
+ return strdup(SDL_GetPlatform());
+}
+
+
// Ok, this thing can have a mess of different arguments:
// WW - Just the width of the white border
// WW WB - Width of white border and then black border
diff --git a/src/midi.c b/src/midi.c
index 39a03e690..516f357dc 100644
--- a/src/midi.c
+++ b/src/midi.c
@@ -152,6 +152,16 @@ bool midiIs(const void *bytes, size_t size) {
}
+void midiQuit(void) {
+ if (_soundfont != NULL) {
+ tsf_close(_soundfont);
+ _soundfont = NULL;
+ }
+ free(_found);
+ _found = NULL;
+}
+
+
bool midiRender(const void *bytes, size_t size, DecodedAudioT *out) {
tml_message *messages = NULL;
tml_message *message = NULL;
@@ -212,16 +222,6 @@ bool midiRender(const void *bytes, size_t size, DecodedAudioT *out) {
}
-void midiQuit(void) {
- if (_soundfont != NULL) {
- tsf_close(_soundfont);
- _soundfont = NULL;
- }
- free(_found);
- _found = NULL;
-}
-
-
const char *midiSoundfont(void) {
return (_found != NULL) ? _found : "none found (MIDI will not play; name one with --soundfont)";
}
diff --git a/src/midiIo.c b/src/midiIo.c
index b3e79ab13..c09e33dfc 100644
--- a/src/midiIo.c
+++ b/src/midiIo.c
@@ -97,14 +97,14 @@ static void _receive(void);
#define ALSA_PORT "Singe MIDI"
#define ALSA_ADDRESS "%d:%d"
-static void *_alsa = NULL;
-static snd_seq_t *_seq = NULL;
-static snd_midi_event_t *_codec = NULL;
-static int32_t _seqPort = -1;
-static int32_t _inClient = -1;
-static int32_t _inPort = -1;
-static int32_t _outClient = -1;
-static int32_t _outPort = -1;
+static void *_alsa = NULL;
+static snd_seq_t *_seq = NULL;
+static snd_midi_event_t *_codec = NULL;
+static int32_t _seqPort = -1;
+static int32_t _inClient = -1;
+static int32_t _inPort = -1;
+static int32_t _outClient = -1;
+static int32_t _outPort = -1;
static int (*_seqOpen)(snd_seq_t **, const char *, int, int);
static int (*_seqClose)(snd_seq_t *);
diff --git a/src/persist.c b/src/persist.c
index 9e9142f96..d21a87e75 100644
--- a/src/persist.c
+++ b/src/persist.c
@@ -116,9 +116,12 @@ bool persistWrite(const char *text, size_t length) {
return false;
}
fclose(file);
- // Windows will not rename onto an existing file, so the old one goes first. The window between
- // the two is the one risk left, and it is smaller than writing in place.
+ // Windows will not rename onto an existing file, so the old one goes first there. The window
+ // between the two is the one risk left, and it is smaller than writing in place. Everywhere
+ // else the rename is atomic and the old save stays whole until the new one is in place.
+#ifdef _WIN32
remove(_path);
+#endif
if (rename(_temporary, _path) != 0) {
remove(_temporary);
return false;
diff --git a/src/render.c b/src/render.c
index b7de22a2b..d614aaf60 100644
--- a/src/render.c
+++ b/src/render.c
@@ -68,6 +68,14 @@ void renderEnd(void) {
}
+void renderSelect(RenderApiT api, const RenderBackendT *backend, SDL_Renderer *renderer) {
+ _api = backend != NULL ? api : RENDER_NONE;
+ _backend = backend;
+ _renderer = renderer;
+ utilTrace("Render: %s", renderApiName());
+}
+
+
SDL_GPUTexture *renderTextureFor(SDL_Texture *texture) {
if (texture == NULL) {
return NULL;
@@ -112,14 +120,6 @@ SDL_Texture *renderWrapTexture(SDL_Renderer *renderer, SDL_GPUTexture *texture,
}
-void renderSelect(RenderApiT api, const RenderBackendT *backend, SDL_Renderer *renderer) {
- _api = backend != NULL ? api : RENDER_NONE;
- _backend = backend;
- _renderer = renderer;
- utilTrace("Render: %s", renderApiName());
-}
-
-
// Every run of rgpu* drawing sits between acquiring a command buffer and submitting it, so that is
// where the context is taken and given back. Bracketing here rather than at the callers means
diff --git a/src/renderGles.c b/src/renderGles.c
index 7d3e26a74..3a33e2791 100644
--- a/src/renderGles.c
+++ b/src/renderGles.c
@@ -159,9 +159,9 @@ typedef struct GlesDeviceS {
static GlesDeviceT _device;
+static GLenum _addressMode(SDL_GPUSamplerAddressMode mode);
static void _applyVertexLayout(GlesRenderPassT *pass);
static void _bindTextureUnit(uint32_t unit, GlesTextureT *texture, SDL_GPUSampler *sampler);
-static GLenum _addressMode(SDL_GPUSamplerAddressMode mode);
static GLenum _blendFactor(SDL_GPUBlendFactor factor);
static GLenum _blendOp(SDL_GPUBlendOp op);
static GLenum _compare(SDL_GPUCompareOp op);
@@ -214,8 +214,8 @@ static void _glesUnmapTransferBuffer(SDL_GPUDevice *device,
static void _glesUploadToBuffer(SDL_GPUCopyPass *copyPass, const SDL_GPUTransferBufferLocation *source, const SDL_GPUBufferRegion *destination, bool cycle);
static void _glesUploadToTexture(SDL_GPUCopyPass *copyPass, const SDL_GPUTextureTransferInfo *source, const SDL_GPUTextureRegion *destination, bool cycle);
static GLenum _primitive(SDL_GPUPrimitiveType type);
-static GLuint _scratchFramebuffer(GLenum binding, GlesTextureT *texture, uint32_t level);
static void _remapFragmentBlocks(GLuint program, const char *fragmentSource);
+static GLuint _scratchFramebuffer(GLenum binding, GlesTextureT *texture, uint32_t level);
static void _setPipelineState(GlesPipelineT *pipeline);
static GLenum _stencilOp(SDL_GPUStencilOp op);
static void _vertexFormat(SDL_GPUVertexElementFormat format, GLint *size, GLenum *type, GLboolean *normalized);
@@ -224,6 +224,56 @@ void renderGlesRestore(void);
bool renderGlesStart(void);
+static GLenum _addressMode(SDL_GPUSamplerAddressMode mode) {
+ switch (mode) {
+ case SDL_GPU_SAMPLERADDRESSMODE_REPEAT: return GL_REPEAT;
+ case SDL_GPU_SAMPLERADDRESSMODE_MIRRORED_REPEAT: return GL_MIRRORED_REPEAT;
+ default: return GL_CLAMP_TO_EDGE;
+ }
+}
+
+
+static void _applyVertexLayout(GlesRenderPassT *pass) {
+ GlesPipelineT *pipeline = pass->pipeline;
+ uint32_t i;
+ uint32_t a;
+
+ if ((pipeline == NULL) || !pass->vertexDirty) {
+ return;
+ }
+ pass->vertexDirty = false;
+ for (i = 0; i < pass->vertexCount; i++) {
+ GlesBufferT *buffer = pass->vertexBuffer[i];
+ uint32_t slot = i;
+
+ if (buffer == NULL) {
+ continue;
+ }
+ glBindBuffer(GL_ARRAY_BUFFER, buffer->name);
+ for (a = 0; a < pipeline->attributeCount; a++) {
+ const SDL_GPUVertexAttribute *attribute = &pipeline->attributes[a];
+ GLboolean normalized;
+ GLenum type;
+ GLint size;
+
+ if (attribute->buffer_slot != slot) {
+ continue;
+ }
+ _vertexFormat(attribute->format, &size, &type, &normalized);
+ glEnableVertexAttribArray(attribute->location);
+ // An integer attribute must be fed as an integer: the scene's joint indices are uvec4 in
+ // the shader, and handing them over as floats is a link-time type mismatch, not a value
+ // one, so the draw is rejected rather than merely wrong.
+ if (((type == GL_UNSIGNED_BYTE) && (normalized == GL_FALSE)) || (type == GL_INT) || (type == GL_UNSIGNED_INT)) {
+ glVertexAttribIPointer(attribute->location, size, type, (GLsizei)pipeline->strides[slot], (const void *)(uintptr_t)(pass->vertexOffset[slot] + attribute->offset));
+ } else {
+ glVertexAttribPointer(attribute->location, size, type, normalized, (GLsizei)pipeline->strides[slot], (const void *)(uintptr_t)(pass->vertexOffset[slot] + attribute->offset));
+ }
+ }
+ }
+}
+
+
static void _bindTextureUnit(uint32_t unit, GlesTextureT *texture, SDL_GPUSampler *sampler) {
GLuint name = (GLuint)(uintptr_t)sampler;
@@ -237,15 +287,6 @@ static void _bindTextureUnit(uint32_t unit, GlesTextureT *texture, SDL_GPUSample
}
-static GLenum _addressMode(SDL_GPUSamplerAddressMode mode) {
- switch (mode) {
- case SDL_GPU_SAMPLERADDRESSMODE_REPEAT: return GL_REPEAT;
- case SDL_GPU_SAMPLERADDRESSMODE_MIRRORED_REPEAT: return GL_MIRRORED_REPEAT;
- default: return GL_CLAMP_TO_EDGE;
- }
-}
-
-
static GLenum _blendFactor(SDL_GPUBlendFactor factor) {
switch (factor) {
case SDL_GPU_BLENDFACTOR_ZERO: return GL_ZERO;
@@ -398,6 +439,9 @@ static GLuint _framebufferFor(const SDL_GPUColorTargetInfo *colour, uint32_t col
// The cache is a fixed size on purpose; overflowing it means a target set is being rebuilt
// every frame, which is a bug worth hearing about rather than hiding behind a bigger array.
utilTrace("Gles: framebuffer cache full");
+ for (index = 0; index < _device.framebufferCount; index++) {
+ glDeleteFramebuffers(1, &_device.framebuffers[index].name);
+ }
_device.framebufferCount = 0;
}
slot = &_device.framebuffers[_device.framebufferCount];
@@ -586,47 +630,6 @@ static void _glesBindVertexBuffers(SDL_GPURenderPass *renderPass, Uint32 firstSl
}
-static void _applyVertexLayout(GlesRenderPassT *pass) {
- GlesPipelineT *pipeline = pass->pipeline;
- uint32_t i;
- uint32_t a;
-
- if ((pipeline == NULL) || !pass->vertexDirty) {
- return;
- }
- pass->vertexDirty = false;
- for (i = 0; i < pass->vertexCount; i++) {
- GlesBufferT *buffer = pass->vertexBuffer[i];
- uint32_t slot = i;
-
- if (buffer == NULL) {
- continue;
- }
- glBindBuffer(GL_ARRAY_BUFFER, buffer->name);
- for (a = 0; a < pipeline->attributeCount; a++) {
- const SDL_GPUVertexAttribute *attribute = &pipeline->attributes[a];
- GLboolean normalized;
- GLenum type;
- GLint size;
-
- if (attribute->buffer_slot != slot) {
- continue;
- }
- _vertexFormat(attribute->format, &size, &type, &normalized);
- glEnableVertexAttribArray(attribute->location);
- // An integer attribute must be fed as an integer: the scene's joint indices are uvec4 in
- // the shader, and handing them over as floats is a link-time type mismatch, not a value
- // one, so the draw is rejected rather than merely wrong.
- if (((type == GL_UNSIGNED_BYTE) && (normalized == GL_FALSE)) || (type == GL_INT) || (type == GL_UNSIGNED_INT)) {
- glVertexAttribIPointer(attribute->location, size, type, (GLsizei)pipeline->strides[slot], (const void *)(uintptr_t)(pass->vertexOffset[slot] + attribute->offset));
- } else {
- glVertexAttribPointer(attribute->location, size, type, normalized, (GLsizei)pipeline->strides[slot], (const void *)(uintptr_t)(pass->vertexOffset[slot] + attribute->offset));
- }
- }
- }
-}
-
-
static void _glesBindVertexStorageBuffers(SDL_GPURenderPass *renderPass, Uint32 firstSlot, SDL_GPUBuffer *const *storageBuffers, Uint32 numBindings) {
uint32_t i;
diff --git a/src/scene.c b/src/scene.c
index 990f9f653..22b49727b 100644
--- a/src/scene.c
+++ b/src/scene.c
@@ -3978,18 +3978,6 @@ bool materialSetBlend(int32_t material, bool blend) {
}
-// glTF's alpha masking: a texel whose base colour alpha falls below the cutoff is discarded, in
-// the lit pass and in the shadow pass alike. Zero turns masking off, which is the default and what
-// every material that never asked for it keeps.
-bool materialSetCutoff(int32_t material, float cutoff) {
- if (!materialValid(material)) {
- return false;
- }
- _scene.materials[material].cutoff = SDL_clamp(cutoff, 0.0f, 1.0f);
- return true;
-}
-
-
bool materialSetColor(int32_t material, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
if (!materialValid(material)) {
return false;
@@ -4015,6 +4003,18 @@ bool materialSetColorLinear(int32_t material, float r, float g, float b, float a
}
+// glTF's alpha masking: a texel whose base colour alpha falls below the cutoff is discarded, in
+// the lit pass and in the shadow pass alike. Zero turns masking off, which is the default and what
+// every material that never asked for it keeps.
+bool materialSetCutoff(int32_t material, float cutoff) {
+ if (!materialValid(material)) {
+ return false;
+ }
+ _scene.materials[material].cutoff = SDL_clamp(cutoff, 0.0f, 1.0f);
+ return true;
+}
+
+
bool materialSetDoubleSided(int32_t material, bool doubleSided) {
if (!materialValid(material)) {
return false;
diff --git a/src/scheduler.c b/src/scheduler.c
index a98ca2848..c9292fcc3 100644
--- a/src/scheduler.c
+++ b/src/scheduler.c
@@ -31,9 +31,11 @@
#include "util.h"
#define SLOTS_FIRST 16 // Grown by doubling from here
+#define SLOT_BITS 16 // A handle is the slot in its low bits and a generation above them:
+#define SLOT_MASK 0xFFFF // a slot that is freed and given out again gets a new handle, so a
+#define GENERATION_MASK 0x7FFF // stale handle a script kept cannot cancel or ask after the newcomer.
#define BACK_OVERSHOOT 1.70158 // How far past the target the "back" easings go, the usual constant
#define ELASTIC_PERIOD 0.3
-#define ELASTIC_AMPLITUDE 0.1
#define BOUNCE_SCALE 7.5625 // The bounce curve's four arcs, as everyone writes them
#define BOUNCE_SPLIT 2.75
#define HALF 0.5
@@ -47,6 +49,7 @@ typedef struct {
double from;
double to;
SchedulerEaseE easing;
+ int32_t generation; // Stepped every time the slot is given out
bool repeating;
bool used;
} SlotT;
@@ -60,24 +63,33 @@ static int32_t _capacity = 0;
static int32_t _allocate(void);
static double _ease(SchedulerEaseE easing, double t);
static double _easeBounceOut(double t);
+static int32_t _handleOf(int32_t slot);
+static SlotT *_slotOf(int32_t handle);
// ===== Internal helpers =====
-// The first free slot, growing the table when there is none. Handles are indexes, so a slot never
-// moves while anything might still name it.
+// The first free slot, growing the table when there is none. A slot never moves while anything
+// might still name it, and each time one is handed out again its generation steps, so the handle
+// a script was given for the old occupant names nothing once that occupant is gone.
static int32_t _allocate(void) {
- SlotT *grown = NULL;
- int32_t want = 0;
- int32_t x = 0;
+ SlotT *grown = NULL;
+ int32_t want = 0;
+ int32_t x = 0;
+ int32_t generation = 0;
for (x = 0; x < _count; x++) {
if (!_slots[x].used) {
+ generation = (_slots[x].generation + 1) & GENERATION_MASK;
memset(&_slots[x], 0, sizeof(_slots[x]));
- _slots[x].used = true;
+ _slots[x].generation = generation;
+ _slots[x].used = true;
return x;
}
}
+ if (_count > SLOT_MASK) {
+ utilDie("Too many timers and tweens are alive at once.");
+ }
if (_count == _capacity) {
want = (_capacity == 0) ? SLOTS_FIRST : (_capacity * 2);
grown = (SlotT *)realloc(_slots, sizeof(SlotT) * (size_t)want);
@@ -155,13 +167,33 @@ static double _easeBounceOut(double t) {
}
+// The handle a script is given for a slot: the slot's index under its generation.
+static int32_t _handleOf(int32_t slot) {
+ return (_slots[slot].generation << SLOT_BITS) | slot;
+}
+
+
+// The slot a handle names, or NULL when it names nothing: out of range, free, or an earlier
+// occupant of a slot that has since been given to something else.
+static SlotT *_slotOf(int32_t handle) {
+ int32_t slot = handle & SLOT_MASK;
+
+ if ((handle < 0) || (slot >= _count) || !_slots[slot].used || (_handleOf(slot) != handle)) {
+ return NULL;
+ }
+
+ return &_slots[slot];
+}
+
+
// ===== Public =====
void schedulerCancel(int32_t handle) {
- if ((handle < 0) || (handle >= _count)) {
- return;
+ SlotT *slot = _slotOf(handle);
+
+ if (slot != NULL) {
+ slot->used = false;
}
- _slots[handle].used = false;
}
@@ -180,11 +212,7 @@ int32_t schedulerCount(void) {
bool schedulerIsActive(int32_t handle) {
- if ((handle < 0) || (handle >= _count)) {
- return false;
- }
-
- return _slots[handle].used;
+ return _slotOf(handle) != NULL;
}
@@ -197,8 +225,8 @@ void schedulerReset(void) {
int32_t schedulerTimer(int64_t milliseconds, bool repeating, uint64_t now) {
- int32_t handle = _allocate();
- SlotT *slot = &_slots[handle];
+ int32_t index = _allocate();
+ SlotT *slot = &_slots[index];
if (milliseconds < 0) {
milliseconds = 0;
@@ -208,13 +236,13 @@ int32_t schedulerTimer(int64_t milliseconds, bool repeating, uint64_t now) {
slot->due = now + (uint64_t)milliseconds;
slot->repeating = repeating;
- return handle;
+ return _handleOf(index);
}
int32_t schedulerTween(double from, double to, int64_t milliseconds, SchedulerEaseE easing, uint64_t now) {
- int32_t handle = _allocate();
- SlotT *slot = &_slots[handle];
+ int32_t index = _allocate();
+ SlotT *slot = &_slots[index];
if (milliseconds < 0) {
milliseconds = 0;
@@ -230,7 +258,7 @@ int32_t schedulerTween(double from, double to, int64_t milliseconds, SchedulerEa
slot->to = to;
slot->easing = easing;
- return handle;
+ return _handleOf(index);
}
@@ -260,7 +288,7 @@ void schedulerUpdate(uint64_t now, SchedulerFireT fire, void *context) {
slot->used = false;
}
if (fire != NULL) {
- fire(context, x, SCHEDULER_TIMER, 0, 0, !slot->repeating);
+ fire(context, _handleOf(x), SCHEDULER_TIMER, 0, 0, !slot->repeating);
}
continue;
}
@@ -274,7 +302,7 @@ void schedulerUpdate(uint64_t now, SchedulerFireT fire, void *context) {
slot->used = false;
}
if (fire != NULL) {
- fire(context, x, SCHEDULER_TWEEN, value, progress, finished);
+ fire(context, _handleOf(x), SCHEDULER_TWEEN, value, progress, finished);
}
}
}
diff --git a/src/singe.c b/src/singe.c
index 1db5f0eee..dd14d6ded 100644
--- a/src/singe.c
+++ b/src/singe.c
@@ -760,12 +760,14 @@ static int32_t _axisIndexForCode(int32_t code);
static void _bezelDraw(void);
static void _bezelLoad(void);
static bool _bezelReadCutout(const char *path, int32_t width, int32_t height, SDL_FRect *cutout);
-static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base);
+static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base, const char *method);
static void _callLua(const char *func, const char *sig, ...);
static bool _clipLine(int32_t *x1, int32_t *y1, int32_t *x2, int32_t *y2);
static void _computeVideoRect(void);
+static void _confFail(lua_State *L, const char *method, const char *fmt, ...) __attribute__((format(printf, 3, 4))) __attribute__((noreturn));
static void _configPlaces(const char *file, const char *dataDir, const char *scriptFile, char **places);
static int32_t _controllerSlot(SDL_JoystickID which);
+static void _createScriptContext(void);
static int32_t _defaultVolume(int32_t maximum);
static bool _delayAndPump(uint32_t ms);
static void _deliverKey(bool down, int32_t keysym, int32_t scancode);
@@ -844,6 +846,7 @@ static bool _luaFileReadNumber(lua_State *L, LuaFileT *file);
static void _luaFileRegister(lua_State *L);
static int32_t _luaFileSearcher(lua_State *L);
static int32_t _luaFileSeek(lua_State *L);
+static void _luaFileShut(LuaFileT *file);
static LuaFileT *_luaFileTest(lua_State *L, int32_t index);
static int32_t _luaFileToString(lua_State *L);
static void _luaFileUngetc(LuaFileT *file);
@@ -851,8 +854,8 @@ static int32_t _luaFileWrite(lua_State *L);
static char *_luaFormat(lua_State *L, const char *method, const char *fmt, va_list args);
static int32_t _luaIoClose(lua_State *L);
static void _luaIoForgetInput(lua_State *L);
-static LuaFileT *_luaIoInput(lua_State *L);
static int32_t _luaIoHook(lua_State *L);
+static LuaFileT *_luaIoInput(lua_State *L);
static int32_t _luaIoRead(lua_State *L);
static int32_t _luaIoSetInput(lua_State *L);
static int32_t _luaIoType(lua_State *L);
@@ -868,29 +871,18 @@ static void _mapPointer(float px, float py, int32_t *x, int32_t *y);
static void _mapPointerDelta(float dx, float dy, int32_t *xr, int32_t *yr);
static void _mapUnknownJoysticks(void);
static int32_t _materialSetMap(lua_State *L, const char *method, MaterialMapE map, bool hasStrength);
-static float _mixerGain(int32_t volume, int32_t maximum);
-static int32_t _mouseCode(int32_t device, int32_t button);
-static void _musicDestroy(MusicT *music);
-static bool _saveCjson(lua_State *L, const char *name);
-static void _saveMachineSettings(int32_t milliseconds);
-static void _saveCopy(lua_State *L, int32_t index, const char *method, int32_t depth);
-static void _saveNormalise(lua_State *L, int32_t depth);
-static void _saveCheckValue(lua_State *L, const char *method, int32_t index);
-static void _saveTable(lua_State *L);
-static void _saveWrite(void);
-static void _scheduleFired(void *context, int32_t handle, SchedulerKindE kind, double value, double progress, bool finished);
-static void _scheduleRemember(lua_State *L, int32_t handle, int32_t updateIndex, int32_t doneIndex);
-static void _scheduleReset(lua_State *L);
static uint8_t _midiChannel(lua_State *L, const char *method, int32_t index);
static uint8_t _midiData(lua_State *L, const char *method, int32_t index);
static void _midiReceived(void *context, const uint8_t *bytes, size_t size);
+static float _mixerGain(int32_t volume, int32_t maximum);
+static int32_t _mouseCode(int32_t device, int32_t button);
+static void _musicDestroy(MusicT *music);
static void _navCallbacks(void);
-static void _statsDraw(void);
static void _noteInput(void);
static void _overlayApplyOpacity(void);
+static void _overlayApplyScaleMode(void);
static uint32_t _overlayColor(const SDL_Color *color);
static SDL_Surface *_overlayGrey(void);
-static void _overlayApplyScaleMode(void);
static void _overlayResize(int32_t width, int32_t height);
static void _overlayTouched(void);
static void _particleTexturesDestroy(ParticleTexturesT *cache);
@@ -916,7 +908,17 @@ static void _resetScriptState(void);
static void _rmlEscape(char *out, size_t size, const char *text);
static void _runScript(bool fatal);
static bool _sameLoosePath(const char *a, const char *b);
+static void _saveCheckValue(lua_State *L, const char *method, int32_t index);
+static bool _saveCjson(lua_State *L, const char *name);
+static void _saveCopy(lua_State *L, int32_t index, const char *method, int32_t depth);
+static void _saveMachineSettings(int32_t milliseconds);
+static void _saveNormalise(lua_State *L, int32_t depth);
+static void _saveTable(lua_State *L);
+static void _saveWrite(void);
static SDL_Texture *_sceneVideoSource(int32_t player);
+static void _scheduleFired(void *context, int32_t handle, SchedulerKindE kind, double value, double progress, bool finished);
+static void _scheduleRemember(lua_State *L, int32_t handle, int32_t updateIndex, int32_t doneIndex);
+static void _scheduleReset(lua_State *L);
static void _scorePanelDigits(char *text, size_t size, int32_t value, int32_t digits, bool blank);
static bool _scorePanelEnable(bool enabled);
static int32_t _scorePanelPlayer(int32_t player);
@@ -947,6 +949,7 @@ static void _spriteRegister(SpriteT *sprite);
static const char *_startAudioSuffix(void);
static void _startControllers(void);
static void _startLuaContext(lua_State *L);
+static void _statsDraw(void);
static void _stopControllers(void);
static void _subsystemsInit(void);
static void _subsystemsQuit(void);
@@ -2114,7 +2117,7 @@ static bool _bezelReadCutout(const char *path, int32_t width, int32_t height, SD
// Builds a config for scriptExecute/scriptPush from the games.dat style table at stack index 1.
-static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base) {
+static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base, const char *method) {
const char *confKey = NULL;
const char *valueString = NULL;
bool valueBoolean = false;
@@ -2166,7 +2169,7 @@ static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base) {
// Update config with new data
if (strcmp(confKey, "SCRIPT") == 0) {
if (valueString == NULL) {
- utilDie("SCRIPT must be a string.");
+ _confFail(L, method, "SCRIPT must be a string.");
}
free(c->scriptFile);
c->scriptFile = strdup(valueString);
@@ -2175,20 +2178,20 @@ static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base) {
// The master service's id for this game. Nothing in the engine uses it; it is carried
// so a script can ask singeGetGameId() which board its scores belong on.
if (valueString == NULL) {
- utilDie("GAME_ID must be a string.");
+ _confFail(L, method, "GAME_ID must be a string.");
}
free(c->gameId);
c->gameId = strdup(valueString);
} else if (strcmp(confKey, "CONTAINER") == 0) {
if (valueString == NULL) {
- utilDie("CONTAINER must be a string.");
+ _confFail(L, method, "CONTAINER must be a string.");
}
free(c->container);
c->container = strdup(valueString);
utilFixPathSeparators(&c->container, false);
} else if (strcmp(confKey, "VIDEO") == 0) {
if (valueString == NULL) {
- utilDie("VIDEO must be a string.");
+ _confFail(L, method, "VIDEO must be a string.");
}
free(c->videoFile);
c->videoFile = strdup(valueString);
@@ -2214,7 +2217,7 @@ static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base) {
c->audioDelayMs = (int32_t)valueNumber;
} else if (strcmp(confKey, "AUDIO_SUFFIX") == 0) {
if (valueString == NULL) {
- utilDie("AUDIO_SUFFIX must be a string.");
+ _confFail(L, method, "AUDIO_SUFFIX must be a string.");
}
free(c->audioSuffix);
c->audioSuffix = strdup(valueString);
@@ -2237,7 +2240,7 @@ static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base) {
}
c->disc = (c->videoFile != NULL);
if ((c->canvasWidth <= 0) || (c->canvasHeight <= 0)) {
- utilDie("%s: CANVAS_X and CANVAS_Y must be positive.", c->scriptFile);
+ _confFail(L, method, "%s: CANVAS_X and CANVAS_Y must be positive.", c->scriptFile);
}
return c;
@@ -2483,6 +2486,22 @@ static void _computeVideoRect(void) {
}
+// A bad games.dat value. From a script it is reported as an API argument error, with the line and
+// the call named; at startup, before there is a script, it ends the run with the message alone.
+static void _confFail(lua_State *L, const char *method, const char *fmt, ...) {
+ char message[256];
+ va_list args;
+
+ va_start(args, fmt);
+ vsnprintf(message, sizeof(message), fmt, args);
+ va_end(args);
+ if (method != NULL) {
+ _luaDie(L, method, "%s", message);
+ }
+ utilDie("%s", message);
+}
+
+
// Maps an SDL joystick instance ID to our controller slot, or -1.
// The four places a user configuration file is read from, in the order they are applied: the
// working directory, above the data directory, inside it, and beside the game's script. A place
@@ -4170,13 +4189,7 @@ static int32_t _luaDofile(lua_State *L) {
static int32_t _luaFileClose(lua_State *L) {
- LuaFileT *file = (LuaFileT *)luaL_checkudata(L, 1, LUA_VFS_FILE);
-
- if (!file->closed) {
- vfsStreamClose(file->stream);
- file->stream = NULL;
- file->closed = true;
- }
+ _luaFileShut((LuaFileT *)luaL_checkudata(L, 1, LUA_VFS_FILE));
lua_pushboolean(L, 1);
return 1;
@@ -4184,13 +4197,7 @@ static int32_t _luaFileClose(lua_State *L) {
static int32_t _luaFileGc(lua_State *L) {
- LuaFileT *file = (LuaFileT *)luaL_checkudata(L, 1, LUA_VFS_FILE);
-
- if (!file->closed) {
- vfsStreamClose(file->stream);
- file->stream = NULL;
- file->closed = true;
- }
+ _luaFileShut((LuaFileT *)luaL_checkudata(L, 1, LUA_VFS_FILE));
return 0;
}
@@ -4224,28 +4231,35 @@ static int32_t _luaFileIterate(lua_State *L) {
if (file->closed) {
return luaL_error(L, "file is already closed");
}
+ // Whatever the generic for passed in is not wanted; the results build up from an empty stack.
+ lua_settop(L, 0);
if (formats == 0) {
if (_luaFileReadLine(L, file, false)) {
return 1;
}
} else {
+ // Each format is pushed, read from where it sits, and then taken out from under its value,
+ // so the values are left in order and nothing read earlier is lost to a later format.
for (i = 0; i < formats; i++) {
lua_pushvalue(L, lua_upvalueindex(4 + i));
- lua_replace(L, 1);
- lua_settop(L, 1);
- if (!_luaFileReadFormat(L, file, 1)) {
- lua_settop(L, 0);
+ if (!_luaFileReadFormat(L, file, lua_gettop(L))) {
+ lua_pop(L, 1);
break;
}
+ lua_remove(L, -2);
}
+ // As Lua does: only the first format coming up empty ends the loop. A later one hands
+ // back what was read before it, with nil in its place.
if (i == formats) {
return formats;
}
+ if (i > 0) {
+ lua_pushnil(L);
+ return i + 1;
+ }
}
if (closing) {
- vfsStreamClose(file->stream);
- file->stream = NULL;
- file->closed = true;
+ _luaFileShut(file);
}
lua_pushnil(L);
@@ -4342,15 +4356,22 @@ static int32_t _luaFileRead(lua_State *L) {
// Reads until the end and pushes what there was. Always a string, empty at the end of the file,
-// which is what Lua's "a" does.
+// which is what Lua's "a" does. What the buffer already holds goes first, and the rest is pulled
+// in blocks: a level file or a save is read whole, and a byte at a time is how that gets slow.
static void _luaFileReadAll(lua_State *L, LuaFileT *file) {
luaL_Buffer out;
- int32_t c = 0;
+ int64_t got = 0;
luaL_buffinit(L, &out);
- while ((c = _luaFileGetc(file)) >= 0) {
- luaL_addchar(&out, (char)c);
+ luaL_addlstring(&out, (const char *)file->buffer + file->at, (size_t)(file->have - file->at));
+ file->pos += file->have - file->at;
+ file->at = file->have;
+ while ((got = vfsStreamRead(file->stream, file->buffer, LUA_VFS_BUFFER)) > 0) {
+ luaL_addlstring(&out, (const char *)file->buffer, (size_t)got);
+ file->pos += got;
}
+ file->have = 0;
+ file->at = 0;
luaL_pushresult(&out);
}
@@ -4362,13 +4383,22 @@ static bool _luaFileReadCount(lua_State *L, LuaFileT *file, int64_t count) {
int64_t read = 0;
int32_t c = 0;
+ // read(0) is Lua's test for the end: an empty string while there is more, nil at the end.
+ if (count == 0) {
+ if (_luaFileGetc(file) < 0) {
+ return false;
+ }
+ _luaFileUngetc(file);
+ lua_pushliteral(L, "");
+ return true;
+ }
luaL_buffinit(L, &out);
while ((read < count) && ((c = _luaFileGetc(file)) >= 0)) {
luaL_addchar(&out, (char)c);
read++;
}
luaL_pushresult(&out);
- if ((read == 0) && (count > 0)) {
+ if (read == 0) {
lua_pop(L, 1);
return false;
}
@@ -4442,7 +4472,7 @@ static bool _luaFileReadNumber(lua_State *L, LuaFileT *file) {
do {
c = _luaFileGetc(file);
} while ((c >= 0) && isspace(c));
- while ((c >= 0) && (length < sizeof(text) - 1) && (isalnum(c) || (strchr("+-.", c) != NULL))) {
+ while ((c > 0) && (length < sizeof(text) - 1) && (isalnum(c) || (strchr("+-.", c) != NULL))) {
text[length++] = (char)c;
c = _luaFileGetc(file);
}
@@ -4572,6 +4602,16 @@ static int32_t _luaFileSeek(lua_State *L) {
}
+// Lets the stream go. Safe to call twice: close, collection and the end of io.lines all reach it.
+static void _luaFileShut(LuaFileT *file) {
+ if (!file->closed) {
+ vfsStreamClose(file->stream);
+ file->stream = NULL;
+ file->closed = true;
+ }
+}
+
+
// The handle, if that is what is at index; NULL for anything else.
static LuaFileT *_luaFileTest(lua_State *L, int32_t index) {
return (LuaFileT *)luaL_testudata(L, index, LUA_VFS_FILE);
@@ -4651,55 +4691,6 @@ static void _luaIoForgetInput(lua_State *L) {
}
-static LuaFileT *_luaIoInput(lua_State *L) {
- LuaFileT *file = NULL;
-
- if (lua_getfield(L, LUA_REGISTRYINDEX, LUA_VFS_INPUT) != LUA_TNIL) {
- file = _luaFileTest(L, -1);
- }
- lua_pop(L, 1);
-
- return file;
-}
-
-
-// io.read(...) -- from ours when io.input was given one, otherwise from Lua's own default input.
-static int32_t _luaIoRead(lua_State *L) {
- if (_luaIoInput(L) == NULL) {
- return _luaCallOriginal(L);
- }
- lua_getfield(L, LUA_REGISTRYINDEX, LUA_VFS_INPUT);
- lua_insert(L, 1);
-
- return _luaFileRead(L);
-}
-
-
-// The handle at 1 becomes the default input, and answers as io.input's result. Lua keeps its own
-// default in the registry and its own functions would take ours for a FILE *, so ours is kept
-// beside it and the three functions that read the default look here first.
-static int32_t _luaIoSetInput(lua_State *L) {
- lua_settop(L, 1);
- lua_pushvalue(L, 1);
- lua_setfield(L, LUA_REGISTRYINDEX, LUA_VFS_INPUT);
-
- return 1;
-}
-
-
-// io.type(x) -- ours answer as files, because to a script that is what they are.
-static int32_t _luaIoType(lua_State *L) {
- LuaFileT *file = _luaFileTest(L, 1);
-
- if (file == NULL) {
- return _luaCallOriginal(L);
- }
- lua_pushstring(L, file->closed ? "closed file" : "file");
-
- return 1;
-}
-
-
// io.open and friends with a name resolved through the vfs. Upvalues: the original, and how the name is used.
static int32_t _luaIoHook(lua_State *L) {
IoHookModeE mode = (IoHookModeE)lua_tointeger(L, lua_upvalueindex(2));
@@ -4772,6 +4763,55 @@ static int32_t _luaIoHook(lua_State *L) {
}
+static LuaFileT *_luaIoInput(lua_State *L) {
+ LuaFileT *file = NULL;
+
+ if (lua_getfield(L, LUA_REGISTRYINDEX, LUA_VFS_INPUT) != LUA_TNIL) {
+ file = _luaFileTest(L, -1);
+ }
+ lua_pop(L, 1);
+
+ return file;
+}
+
+
+// io.read(...) -- from ours when io.input was given one, otherwise from Lua's own default input.
+static int32_t _luaIoRead(lua_State *L) {
+ if (_luaIoInput(L) == NULL) {
+ return _luaCallOriginal(L);
+ }
+ lua_getfield(L, LUA_REGISTRYINDEX, LUA_VFS_INPUT);
+ lua_insert(L, 1);
+
+ return _luaFileRead(L);
+}
+
+
+// The handle at 1 becomes the default input, and answers as io.input's result. Lua keeps its own
+// default in the registry and its own functions would take ours for a FILE *, so ours is kept
+// beside it and the three functions that read the default look here first.
+static int32_t _luaIoSetInput(lua_State *L) {
+ lua_settop(L, 1);
+ lua_pushvalue(L, 1);
+ lua_setfield(L, LUA_REGISTRYINDEX, LUA_VFS_INPUT);
+
+ return 1;
+}
+
+
+// io.type(x) -- ours answer as files, because to a script that is what they are.
+static int32_t _luaIoType(lua_State *L) {
+ LuaFileT *file = _luaFileTest(L, 1);
+
+ if (file == NULL) {
+ return _luaCallOriginal(L);
+ }
+ lua_pushstring(L, file->closed ? "closed file" : "file");
+
+ return 1;
+}
+
+
// loadfile(name, mode, env) through the vfs; without a name the original reads stdin.
static int32_t _luaLoadfile(lua_State *L) {
const char *name = luaL_optstring(L, 1, NULL);
@@ -5006,6 +5046,39 @@ static int32_t _materialSetMap(lua_State *L, const char *method, MaterialMapE ma
}
+// A MIDI channel argument, which a person counts from 1 and the wire counts from 0.
+static uint8_t _midiChannel(lua_State *L, const char *method, int32_t index) {
+ int32_t channel = _argInteger(L, method, index) - 1;
+
+ if ((channel < 0) || (channel > MIDI_CHANNEL_MAX)) {
+ _luaDie(L, method, "A MIDI channel is 1 to %d, not %d.", MIDI_CHANNEL_MAX + 1, channel + 1);
+ }
+
+ return (uint8_t)channel;
+}
+
+
+// A seven bit MIDI data argument: a key, a velocity, a controller or its value.
+static uint8_t _midiData(lua_State *L, const char *method, int32_t index) {
+ int32_t value = _argInteger(L, method, index);
+
+ if ((value < 0) || (value > MIDI_DATA_MAX)) {
+ _luaDie(L, method, "A MIDI value is 0 to %d, not %d.", MIDI_DATA_MAX, value);
+ }
+
+ return (uint8_t)value;
+}
+
+
+// onMidiMessage(status, data1, data2, bytes) for one message that arrived on the input port. The
+// three numbers are the usual message; the string is the whole thing, which is what a system
+// exclusive message needs.
+static void _midiReceived(void *context, const uint8_t *bytes, size_t size) {
+ (void)context;
+ _callLua("onMidiMessage", "iiiS", (int32_t)bytes[0], (size > 1) ? (int32_t)bytes[1] : 0, (size > 2) ? (int32_t)bytes[2] : 0, (const char *)bytes, size);
+}
+
+
// Converts a script visible volume to the mixer's 0 to 1 gain. The two families keep different
// scales: sound effects run to AUDIO_MAX_VOLUME and music to MUSIC_MAX_VOLUME, which is Hypseus's.
static float _mixerGain(int32_t volume, int32_t maximum) {
@@ -5019,38 +5092,14 @@ static int32_t _mouseCode(int32_t device, int32_t button) {
}
-// Everything the developer's overlay shows, gathered from wherever it lives. Drawn after the
-// screenshot is taken, so a reference shot never has the overlay in it.
-static void _statsDraw(void) {
- StatsT stats;
- int64_t textureBytes = 0;
- SpriteT *sprite = NULL;
- SpriteT *spriteNext = NULL;
- SoundT *sound = NULL;
- SoundT *soundNext = NULL;
-
- if (!statsIsEnabled()) {
- return;
- }
- memset(&stats, 0, sizeof(stats));
- stats.frameMilliseconds = _global.statsFrameMs;
- stats.worstMilliseconds = statsWorstMilliseconds();
- stats.framesPerSecond = statsFramesPerSecond();
- stats.luaKilobytes = (_global.luaContext != NULL) ? (double)lua_gc(_global.luaContext, LUA_GCCOUNT) : 0;
- stats.timers = schedulerCount();
- HASH_ITER(hh, _global.spriteList, sprite, spriteNext) {
- stats.sprites++;
- }
- HASH_ITER(hh, _global.soundList, sound, soundNext) {
- stats.sounds++;
- }
- sceneGetStats(&stats.nodesTotal, &stats.nodesDrawn, &stats.batches, &textureBytes);
- stats.textureKilobytes = textureBytes / BYTES_PER_KIB;
- if (_global.videoHandle >= 0) {
- stats.discFrame = videoGetFrame(_global.videoHandle);
- stats.discState = _global.discStopped ? "stopped" : (videoIsPlaying(_global.videoHandle) ? "playing" : "paused");
- }
- statsDraw(_global.renderer, &stats);
+// Stops a music track, frees it and forgets it.
+static void _musicDestroy(MusicT *music) {
+ HASH_DEL(_global.musicList, music);
+ MIX_StopTrack(music->track, 0);
+ MIX_SetTrackAudio(music->track, NULL);
+ MIX_DestroyTrack(music->track);
+ MIX_DestroyAudio(music->audio);
+ free(music);
}
@@ -5076,23 +5125,18 @@ static void _noteInput(void) {
}
-// Stops a music track, frees it and forgets it.
-static void _musicDestroy(MusicT *music) {
- HASH_DEL(_global.musicList, music);
- MIX_StopTrack(music->track, 0);
- MIX_SetTrackAudio(music->track, NULL);
- MIX_DestroyTrack(music->track);
- MIX_DestroyAudio(music->audio);
- free(music);
-}
-
-
// Applies setOverlayOpacity's level to the overlay texture. Called again whenever the texture is made.
static void _overlayApplyOpacity(void) {
SDL_SetTextureAlphaMod(_global.overlayTexture, _global.overlayOpacity);
}
+// Applies setOverlayLinearScale's choice to the overlay texture. Called again whenever the texture is made.
+static void _overlayApplyScaleMode(void) {
+ SDL_SetTextureScaleMode(_global.overlayTexture, _global.overlayLinearScale ? SDL_SCALEMODE_LINEAR : SDL_SCALEMODE_NEAREST);
+}
+
+
// A colour as the overlay surface stores it.
static uint32_t _overlayColor(const SDL_Color *color) {
return SDL_MapRGBA(SDL_GetPixelFormatDetails(_global.overlay->format), NULL, color->r, color->g, color->b, color->a);
@@ -5132,12 +5176,6 @@ static SDL_Surface *_overlayGrey(void) {
}
-// Applies setOverlayLinearScale's choice to the overlay texture. Called again whenever the texture is made.
-static void _overlayApplyScaleMode(void) {
- SDL_SetTextureScaleMode(_global.overlayTexture, _global.overlayLinearScale ? SDL_SCALEMODE_LINEAR : SDL_SCALEMODE_NEAREST);
-}
-
-
// Replaces the overlay surface and texture (and the scene's targets) at a new size; its contents are lost.
static void _overlayResize(int32_t width, int32_t height) {
SDL_DestroySurface(_global.overlayGrey);
@@ -6468,6 +6506,67 @@ static bool _sameLoosePath(const char *a, const char *b) {
}
+// A value a script wants kept. Tables are allowed because a save is usually a table of them; a
+// function or a handle to something the engine owns is not, because neither means anything on the
+// next run.
+static void _saveCheckValue(lua_State *L, const char *method, int32_t index) {
+ int32_t type = lua_type(L, index);
+
+ if ((type != LUA_TNUMBER) && (type != LUA_TSTRING) && (type != LUA_TBOOLEAN) && (type != LUA_TTABLE)) {
+ _luaDie(L, method, "A saved value is a number, a string, a boolean or a table of them, not a %s.", lua_typename(L, type));
+ }
+}
+
+
+// Pushes one of cjson's functions. The save file is JSON because a person should be able to read
+// and fix one, and cjson is already bundled and already knows every corner of the format.
+static bool _saveCjson(lua_State *L, const char *name) {
+ lua_getglobal(L, "require");
+ lua_pushstring(L, "cjson");
+ if (lua_pcall(L, 1, 1, 0) != LUA_OK) {
+ lua_pop(L, 1);
+ return false;
+ }
+ lua_getfield(L, -1, name);
+ lua_remove(L, -2);
+ if (!lua_isfunction(L, -1)) {
+ lua_pop(L, 1);
+ return false;
+ }
+
+ return true;
+}
+
+
+// A deep copy of the value at "index", pushed on the stack. Everything going into the save and
+// everything coming out of it is copied, so the table the engine keeps is its own: what a script
+// sets is what is saved, and what it gets back is its own to change. Sharing the table instead
+// made an edit through saveGet or saveGetAll persist or not depending on whether something unrelated
+// saved later in the same run, which is not a rule anyone could hold in their head.
+static void _saveCopy(lua_State *L, int32_t index, const char *method, int32_t depth) {
+ int32_t source = lua_absindex(L, index);
+
+ if (!lua_istable(L, source)) {
+ lua_pushvalue(L, source);
+ return;
+ }
+ if (depth >= SAVE_DEPTH_MAX) {
+ _luaDie(L, method, "A saved table is nested more than %d deep, or refers to itself.", SAVE_DEPTH_MAX);
+ }
+ lua_newtable(L);
+ lua_pushnil(L);
+ while (lua_next(L, source) != 0) {
+ // The table, its key and its value are on the stack; the copy of the value goes beside
+ // them, then the key is put back on top so the pair can be written into the new table.
+ _saveCopy(L, -1, method, depth + 1);
+ lua_pushvalue(L, -3);
+ lua_insert(L, -2);
+ lua_rawset(L, -5);
+ lua_pop(L, 1);
+ }
+}
+
+
static void _saveMachineSettings(int32_t milliseconds) {
char *path = _machineFilePath();
FILE *out = fopen(path, "w");
@@ -6488,6 +6587,99 @@ static void _saveMachineSettings(int32_t milliseconds) {
}
+// JSON has one kind of number and Lua 5.4 has two, so everything comes back from a decode as a
+// float: a score saved as 100 would return 100.0 and print with a decimal point. Whole numbers are
+// put back to integers as they are read, which is what the game that saved them had. Recursive,
+// because a save is usually a table of tables, with a depth limit for a file somebody hand edited
+// into a spiral.
+static void _saveNormalise(lua_State *L, int32_t depth) {
+ lua_Number number = 0;
+ lua_Integer whole = 0;
+
+ if (depth > SAVE_DEPTH_MAX) {
+ return;
+ }
+ lua_pushnil(L);
+ while (lua_next(L, -2) != 0) {
+ if (lua_istable(L, -1)) {
+ _saveNormalise(L, depth + 1);
+ } else if (lua_isnumber(L, -1) && !lua_isinteger(L, -1)) {
+ number = lua_tonumber(L, -1);
+ whole = (lua_Integer)number;
+ if ((lua_Number)whole == number) {
+ // The key is still below the value, so the value is replaced in place rather than
+ // set again, which would disturb the traversal.
+ lua_pop(L, 1);
+ lua_pushinteger(L, whole);
+ lua_pushvalue(L, -2);
+ lua_pushvalue(L, -2);
+ lua_rawset(L, -5);
+ }
+ }
+ lua_pop(L, 1);
+ }
+}
+
+
+// The save table, read from the file the first time anything asks for it. Always leaves a table on
+// the stack: a save that will not parse is reported and then treated as an empty one, because a
+// game that cannot start is worse than a game that lost its high scores.
+static void _saveTable(lua_State *L) {
+ char *text = NULL;
+
+ lua_getfield(L, LUA_REGISTRYINDEX, SAVE_TABLE);
+ if (lua_istable(L, -1)) {
+ return;
+ }
+ lua_pop(L, 1);
+ lua_newtable(L);
+ text = persistRead();
+ if (text != NULL) {
+ if (_saveCjson(L, "decode")) {
+ lua_pushstring(L, text);
+ if ((lua_pcall(L, 1, 1, 0) == LUA_OK) && lua_istable(L, -1)) {
+ lua_remove(L, -2);
+ _saveNormalise(L, 0);
+ } else {
+ utilSay("Warning: %s could not be read; starting with an empty save.", persistPath());
+ lua_pop(L, 1);
+ }
+ }
+ free(text);
+ }
+ lua_pushvalue(L, -1);
+ lua_setfield(L, LUA_REGISTRYINDEX, SAVE_TABLE);
+}
+
+
+// Writes the save table out. Called at the end of a frame that changed it, and at shutdown.
+static void _saveWrite(void) {
+ lua_State *L = _global.luaContext;
+ const char *text = NULL;
+ size_t size = 0;
+
+ if ((L == NULL) || !_global.saveDirty) {
+ return;
+ }
+ _global.saveDirty = false;
+ if (!_saveCjson(L, "encode")) {
+ utilSay("Warning: the save file needs the cjson module, which did not load.");
+ return;
+ }
+ _saveTable(L);
+ if (lua_pcall(L, 1, 1, 0) != LUA_OK) {
+ utilSay("Warning: the save could not be turned into JSON: %s", lua_tostring(L, -1));
+ lua_pop(L, 1);
+ return;
+ }
+ text = lua_tolstring(L, -1, &size);
+ if ((text != NULL) && !persistWrite(text, size)) {
+ utilSay("Warning: %s could not be written.", persistPath());
+ }
+ lua_pop(L, 1);
+}
+
+
// A player's current frame for the 3D scene's video materials: the disc's texture is already
// updated for this frame; a loaded video is advanced here (drawing it on the overlay too is harmless).
static SDL_Texture *_sceneVideoSource(int32_t player) {
@@ -6506,6 +6698,102 @@ static SDL_Texture *_sceneVideoSource(int32_t player) {
}
+// A timer or a tween has fired. Called from the frame loop by way of the scheduler, never from
+// another thread, so this is an ordinary Lua call.
+static void _scheduleFired(void *context, int32_t handle, SchedulerKindE kind, double value, double progress, bool finished) {
+ lua_State *L = _global.luaContext;
+ int32_t handler;
+ int32_t slot = (finished && (kind == SCHEDULER_TWEEN)) ? SCHEDULE_DONE : SCHEDULE_UPDATE;
+
+ (void)context;
+ if (L == NULL) {
+ return;
+ }
+ if (!luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS)) {
+ lua_pop(L, 1);
+ return;
+ }
+ lua_rawgeti(L, -1, handle);
+ lua_remove(L, -2);
+ if (!lua_istable(L, -1)) {
+ lua_pop(L, 1);
+ return;
+ }
+ // A finished tween calls its update one last time at the end value, and then the optional
+ // second function; a game that only wants the end result gives just that one.
+ if ((slot == SCHEDULE_DONE) && (kind == SCHEDULER_TWEEN)) {
+ lua_rawgeti(L, -1, SCHEDULE_UPDATE);
+ if (lua_isfunction(L, -1)) {
+ lua_pushcfunction(L, _luaTraceback);
+ lua_insert(L, -2);
+ handler = lua_gettop(L) - 1;
+ lua_pushnumber(L, value);
+ lua_pushnumber(L, progress);
+ if (lua_pcall(L, 2, 0, handler) != LUA_OK) {
+ utilDie("Error in a tween: %s", lua_tostring(L, -1));
+ }
+ lua_remove(L, handler);
+ } else {
+ lua_pop(L, 1);
+ }
+ }
+ lua_rawgeti(L, -1, slot);
+ if (!lua_isfunction(L, -1)) {
+ lua_pop(L, 2);
+ return;
+ }
+ lua_pushcfunction(L, _luaTraceback);
+ lua_insert(L, -2);
+ handler = lua_gettop(L) - 1;
+ if (kind == SCHEDULER_TWEEN) {
+ lua_pushnumber(L, value);
+ lua_pushnumber(L, progress);
+ if (lua_pcall(L, 2, 0, handler) != LUA_OK) {
+ utilDie("Error in a tween: %s", lua_tostring(L, -1));
+ }
+ } else {
+ lua_pushinteger(L, handle);
+ if (lua_pcall(L, 1, 0, handler) != LUA_OK) {
+ utilDie("Error in a timer: %s", lua_tostring(L, -1));
+ }
+ }
+ lua_remove(L, handler);
+ lua_pop(L, 1);
+ if (finished) {
+ luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
+ lua_pushnil(L);
+ lua_rawseti(L, -2, handle);
+ lua_pop(L, 1);
+ }
+}
+
+
+// Remembers the functions a timer or tween will call, under its handle.
+static void _scheduleRemember(lua_State *L, int32_t handle, int32_t updateIndex, int32_t doneIndex) {
+ luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
+ lua_newtable(L);
+ lua_pushvalue(L, updateIndex);
+ lua_rawseti(L, -2, SCHEDULE_UPDATE);
+ if ((doneIndex > 0) && lua_isfunction(L, doneIndex)) {
+ lua_pushvalue(L, doneIndex);
+ lua_rawseti(L, -2, SCHEDULE_DONE);
+ }
+ lua_rawseti(L, -2, handle);
+ lua_pop(L, 1);
+}
+
+
+// Everything a timer or tween left behind, for a script reload.
+static void _scheduleReset(lua_State *L) {
+ schedulerReset();
+ if (L == NULL) {
+ return;
+ }
+ lua_newtable(L);
+ lua_setfield(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
+}
+
+
// One readout's digits: the value right justified in the places that readout has, with leading
// zeros the way an arcade board shows them, the symbol Hypseus puts on every digit for each of its
// negative codes, or nothing at all while the panel is blanked.
@@ -6719,7 +7007,7 @@ static ConfigT *_scriptConfFromTable(lua_State *L, const char *method) {
if (!lua_istable(L, 1)) {
_luaDie(L, method, "Argument 1 must be a table.");
}
- conf = _buildConfFromTable(L, _global.conf);
+ conf = _buildConfFromTable(L, _global.conf, method);
conf->dataDir = resolveDataDir(conf);
if (conf->dataDir == NULL) {
_luaDie(L, method, "Unable to create the data directory for %s.", conf->scriptFile);
@@ -7021,289 +7309,6 @@ static SpriteT *_spriteLoadFromIO(lua_State *L, const char *method, SDL_IOStream
}
-// A MIDI channel argument, which a person counts from 1 and the wire counts from 0.
-static uint8_t _midiChannel(lua_State *L, const char *method, int32_t index) {
- int32_t channel = _argInteger(L, method, index) - 1;
-
- if ((channel < 0) || (channel > MIDI_CHANNEL_MAX)) {
- _luaDie(L, method, "A MIDI channel is 1 to %d, not %d.", MIDI_CHANNEL_MAX + 1, channel + 1);
- }
-
- return (uint8_t)channel;
-}
-
-
-// A seven bit MIDI data argument: a key, a velocity, a controller or its value.
-static uint8_t _midiData(lua_State *L, const char *method, int32_t index) {
- int32_t value = _argInteger(L, method, index);
-
- if ((value < 0) || (value > MIDI_DATA_MAX)) {
- _luaDie(L, method, "A MIDI value is 0 to %d, not %d.", MIDI_DATA_MAX, value);
- }
-
- return (uint8_t)value;
-}
-
-
-// onMidiMessage(status, data1, data2, bytes) for one message that arrived on the input port. The
-// three numbers are the usual message; the string is the whole thing, which is what a system
-// exclusive message needs.
-static void _midiReceived(void *context, const uint8_t *bytes, size_t size) {
- (void)context;
- _callLua("onMidiMessage", "iiiS", (int32_t)bytes[0], (size > 1) ? (int32_t)bytes[1] : 0, (size > 2) ? (int32_t)bytes[2] : 0, (const char *)bytes, size);
-}
-
-
-// Pushes one of cjson's functions. The save file is JSON because a person should be able to read
-// and fix one, and cjson is already bundled and already knows every corner of the format.
-static bool _saveCjson(lua_State *L, const char *name) {
- lua_getglobal(L, "require");
- lua_pushstring(L, "cjson");
- if (lua_pcall(L, 1, 1, 0) != LUA_OK) {
- lua_pop(L, 1);
- return false;
- }
- lua_getfield(L, -1, name);
- lua_remove(L, -2);
- if (!lua_isfunction(L, -1)) {
- lua_pop(L, 1);
- return false;
- }
-
- return true;
-}
-
-
-// A deep copy of the value at "index", pushed on the stack. Everything going into the save and
-// everything coming out of it is copied, so the table the engine keeps is its own: what a script
-// sets is what is saved, and what it gets back is its own to change. Sharing the table instead
-// made an edit through saveGet or saveGetAll persist or not depending on whether something unrelated
-// saved later in the same run, which is not a rule anyone could hold in their head.
-static void _saveCopy(lua_State *L, int32_t index, const char *method, int32_t depth) {
- int32_t source = lua_absindex(L, index);
-
- if (!lua_istable(L, source)) {
- lua_pushvalue(L, source);
- return;
- }
- if (depth >= SAVE_DEPTH_MAX) {
- _luaDie(L, method, "A saved table is nested more than %d deep, or refers to itself.", SAVE_DEPTH_MAX);
- }
- lua_newtable(L);
- lua_pushnil(L);
- while (lua_next(L, source) != 0) {
- // The table, its key and its value are on the stack; the copy of the value goes beside
- // them, then the key is put back on top so the pair can be written into the new table.
- _saveCopy(L, -1, method, depth + 1);
- lua_pushvalue(L, -3);
- lua_insert(L, -2);
- lua_rawset(L, -5);
- lua_pop(L, 1);
- }
-}
-
-
-// JSON has one kind of number and Lua 5.4 has two, so everything comes back from a decode as a
-// float: a score saved as 100 would return 100.0 and print with a decimal point. Whole numbers are
-// put back to integers as they are read, which is what the game that saved them had. Recursive,
-// because a save is usually a table of tables, with a depth limit for a file somebody hand edited
-// into a spiral.
-static void _saveNormalise(lua_State *L, int32_t depth) {
- lua_Number number = 0;
- lua_Integer whole = 0;
-
- if (depth > SAVE_DEPTH_MAX) {
- return;
- }
- lua_pushnil(L);
- while (lua_next(L, -2) != 0) {
- if (lua_istable(L, -1)) {
- _saveNormalise(L, depth + 1);
- } else if (lua_isnumber(L, -1) && !lua_isinteger(L, -1)) {
- number = lua_tonumber(L, -1);
- whole = (lua_Integer)number;
- if ((lua_Number)whole == number) {
- // The key is still below the value, so the value is replaced in place rather than
- // set again, which would disturb the traversal.
- lua_pop(L, 1);
- lua_pushinteger(L, whole);
- lua_pushvalue(L, -2);
- lua_pushvalue(L, -2);
- lua_rawset(L, -5);
- }
- }
- lua_pop(L, 1);
- }
-}
-
-
-// The save table, read from the file the first time anything asks for it. Always leaves a table on
-// the stack: a save that will not parse is reported and then treated as an empty one, because a
-// game that cannot start is worse than a game that lost its high scores.
-static void _saveTable(lua_State *L) {
- char *text = NULL;
-
- lua_getfield(L, LUA_REGISTRYINDEX, SAVE_TABLE);
- if (lua_istable(L, -1)) {
- return;
- }
- lua_pop(L, 1);
- lua_newtable(L);
- text = persistRead();
- if (text != NULL) {
- if (_saveCjson(L, "decode")) {
- lua_pushstring(L, text);
- if ((lua_pcall(L, 1, 1, 0) == LUA_OK) && lua_istable(L, -1)) {
- lua_remove(L, -2);
- _saveNormalise(L, 0);
- } else {
- utilSay("Warning: %s could not be read; starting with an empty save.", persistPath());
- lua_pop(L, 1);
- }
- }
- free(text);
- }
- lua_pushvalue(L, -1);
- lua_setfield(L, LUA_REGISTRYINDEX, SAVE_TABLE);
-}
-
-
-// Writes the save table out. Called at the end of a frame that changed it, and at shutdown.
-static void _saveWrite(void) {
- lua_State *L = _global.luaContext;
- const char *text = NULL;
- size_t size = 0;
-
- if ((L == NULL) || !_global.saveDirty) {
- return;
- }
- _global.saveDirty = false;
- if (!_saveCjson(L, "encode")) {
- utilSay("Warning: the save file needs the cjson module, which did not load.");
- return;
- }
- _saveTable(L);
- if (lua_pcall(L, 1, 1, 0) != LUA_OK) {
- utilSay("Warning: the save could not be turned into JSON: %s", lua_tostring(L, -1));
- lua_pop(L, 1);
- return;
- }
- text = lua_tolstring(L, -1, &size);
- if ((text != NULL) && !persistWrite(text, size)) {
- utilSay("Warning: %s could not be written.", persistPath());
- }
- lua_pop(L, 1);
-}
-
-
-// A value a script wants kept. Tables are allowed because a save is usually a table of them; a
-// function or a handle to something the engine owns is not, because neither means anything on the
-// next run.
-static void _saveCheckValue(lua_State *L, const char *method, int32_t index) {
- int32_t type = lua_type(L, index);
-
- if ((type != LUA_TNUMBER) && (type != LUA_TSTRING) && (type != LUA_TBOOLEAN) && (type != LUA_TTABLE)) {
- _luaDie(L, method, "A saved value is a number, a string, a boolean or a table of them, not a %s.", lua_typename(L, type));
- }
-}
-
-
-// A timer or a tween has fired. Called from the frame loop by way of the scheduler, never from
-// another thread, so this is an ordinary Lua call.
-static void _scheduleFired(void *context, int32_t handle, SchedulerKindE kind, double value, double progress, bool finished) {
- lua_State *L = _global.luaContext;
- int32_t handler;
- int32_t slot = (finished && (kind == SCHEDULER_TWEEN)) ? SCHEDULE_DONE : SCHEDULE_UPDATE;
-
- (void)context;
- if (L == NULL) {
- return;
- }
- if (!luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS)) {
- lua_pop(L, 1);
- return;
- }
- lua_rawgeti(L, -1, handle);
- lua_remove(L, -2);
- if (!lua_istable(L, -1)) {
- lua_pop(L, 1);
- return;
- }
- // A finished tween calls its update one last time at the end value, and then the optional
- // second function; a game that only wants the end result gives just that one.
- if ((slot == SCHEDULE_DONE) && (kind == SCHEDULER_TWEEN)) {
- lua_rawgeti(L, -1, SCHEDULE_UPDATE);
- if (lua_isfunction(L, -1)) {
- lua_pushcfunction(L, _luaTraceback);
- lua_insert(L, -2);
- handler = lua_gettop(L) - 1;
- lua_pushnumber(L, value);
- lua_pushnumber(L, progress);
- if (lua_pcall(L, 2, 0, handler) != LUA_OK) {
- utilDie("Error in a tween: %s", lua_tostring(L, -1));
- }
- lua_remove(L, handler);
- } else {
- lua_pop(L, 1);
- }
- }
- lua_rawgeti(L, -1, slot);
- if (!lua_isfunction(L, -1)) {
- lua_pop(L, 2);
- return;
- }
- lua_pushcfunction(L, _luaTraceback);
- lua_insert(L, -2);
- handler = lua_gettop(L) - 1;
- if (kind == SCHEDULER_TWEEN) {
- lua_pushnumber(L, value);
- lua_pushnumber(L, progress);
- if (lua_pcall(L, 2, 0, handler) != LUA_OK) {
- utilDie("Error in a tween: %s", lua_tostring(L, -1));
- }
- } else {
- lua_pushinteger(L, handle);
- if (lua_pcall(L, 1, 0, handler) != LUA_OK) {
- utilDie("Error in a timer: %s", lua_tostring(L, -1));
- }
- }
- lua_remove(L, handler);
- lua_pop(L, 1);
- if (finished) {
- luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
- lua_pushnil(L);
- lua_rawseti(L, -2, handle);
- lua_pop(L, 1);
- }
-}
-
-
-// Remembers the functions a timer or tween will call, under its handle.
-static void _scheduleRemember(lua_State *L, int32_t handle, int32_t updateIndex, int32_t doneIndex) {
- luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
- lua_newtable(L);
- lua_pushvalue(L, updateIndex);
- lua_rawseti(L, -2, SCHEDULE_UPDATE);
- if ((doneIndex > 0) && lua_isfunction(L, doneIndex)) {
- lua_pushvalue(L, doneIndex);
- lua_rawseti(L, -2, SCHEDULE_DONE);
- }
- lua_rawseti(L, -2, handle);
- lua_pop(L, 1);
-}
-
-
-// Everything a timer or tween left behind, for a script reload.
-static void _scheduleReset(lua_State *L) {
- schedulerReset();
- if (L == NULL) {
- return;
- }
- lua_newtable(L);
- lua_setfield(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
-}
-
-
// An empty sprite record for a loader to fill.
static SpriteT *_spriteNew(lua_State *L, const char *method) {
SpriteT *sprite = (SpriteT *)calloc(1, sizeof(SpriteT));
@@ -7446,6 +7451,41 @@ static void _startLuaContext(lua_State *L) {
}
+// Everything the developer's overlay shows, gathered from wherever it lives. Drawn after the
+// screenshot is taken, so a reference shot never has the overlay in it.
+static void _statsDraw(void) {
+ StatsT stats;
+ int64_t textureBytes = 0;
+ SpriteT *sprite = NULL;
+ SpriteT *spriteNext = NULL;
+ SoundT *sound = NULL;
+ SoundT *soundNext = NULL;
+
+ if (!statsIsEnabled()) {
+ return;
+ }
+ memset(&stats, 0, sizeof(stats));
+ stats.frameMilliseconds = _global.statsFrameMs;
+ stats.worstMilliseconds = statsWorstMilliseconds();
+ stats.framesPerSecond = statsFramesPerSecond();
+ stats.luaKilobytes = (_global.luaContext != NULL) ? (double)lua_gc(_global.luaContext, LUA_GCCOUNT) : 0;
+ stats.timers = schedulerCount();
+ HASH_ITER(hh, _global.spriteList, sprite, spriteNext) {
+ stats.sprites++;
+ }
+ HASH_ITER(hh, _global.soundList, sound, soundNext) {
+ stats.sounds++;
+ }
+ sceneGetStats(&stats.nodesTotal, &stats.nodesDrawn, &stats.batches, &textureBytes);
+ stats.textureKilobytes = textureBytes / BYTES_PER_KIB;
+ if (_global.videoHandle >= 0) {
+ stats.discFrame = videoGetFrame(_global.videoHandle);
+ stats.discState = _global.discStopped ? "stopped" : (videoIsPlaying(_global.videoHandle) ? "playing" : "paused");
+ }
+ statsDraw(_global.renderer, &stats);
+}
+
+
static void _stopControllers(void) {
int32_t x = 0;
@@ -15734,7 +15774,7 @@ ConfigT *confFromDatabase(const ConfigT *conf) {
}
lua_replace(L, 1);
lua_settop(L, 1);
- result = _buildConfFromTable(L, base);
+ result = _buildConfFromTable(L, base, NULL);
if (result->scriptFile == NULL) {
utilDie("%s: games.dat entry %d has no SCRIPT.", base->container, conf->entry);
}
@@ -15831,7 +15871,7 @@ ConfigT *confFromGamesDat(const ConfigT *conf) {
if (found) {
lua_replace(L, 1);
lua_settop(L, 1);
- result = _buildConfFromTable(L, conf);
+ result = _buildConfFromTable(L, conf, NULL);
result->entry = entry;
} else {
lua_pop(L, 1);
diff --git a/src/stats.c b/src/stats.c
index f0b1467e6..2391dd568 100644
--- a/src/stats.c
+++ b/src/stats.c
@@ -29,6 +29,7 @@
#define HISTORY 120 // Frames kept, about two seconds at sixty
#define MARGIN 8
+#define PADDING 4 // Between the backdrop edge and the text
#define LINE_HEIGHT (SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE + 2)
#define PANEL_WIDTH 300 // Wide enough for the longest line, which is the frame times
#define LINES_MAX 6 // What statsDraw writes; the backdrop is sized from it
@@ -50,7 +51,7 @@ static void _line(SDL_Renderer *renderer, int32_t index, const char *text);
static void _line(SDL_Renderer *renderer, int32_t index, const char *text) {
SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE);
- SDL_RenderDebugText(renderer, (float)(MARGIN + 4), (float)(MARGIN + 4 + (index * LINE_HEIGHT)), text);
+ SDL_RenderDebugText(renderer, (float)(MARGIN + PADDING), (float)(MARGIN + PADDING + (index * LINE_HEIGHT)), text);
}
@@ -67,7 +68,7 @@ void statsDraw(SDL_Renderer *renderer, const StatsT *stats) {
backdrop.x = MARGIN;
backdrop.y = MARGIN;
backdrop.w = PANEL_WIDTH;
- backdrop.h = (float)((LINES_MAX * LINE_HEIGHT) + 8);
+ backdrop.h = (float)((LINES_MAX * LINE_HEIGHT) + (PADDING * 2));
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, BACKDROP_ALPHA);
SDL_RenderFillRect(renderer, &backdrop);
diff --git a/src/vfs.c b/src/vfs.c
index fc4a62910..495302879 100644
--- a/src/vfs.c
+++ b/src/vfs.c
@@ -127,7 +127,7 @@ static int _listCompare(const void *a, const void *b); // qsort callba
static void _listDirectory(const char *path, ListT *list);
static char **_listFinish(ListT *list, int32_t *count);
static char *_normalise(const char *name);
-static char *_overlayFor(const char *dataDirBase, const char *dataDir, const char *databasePath, bool isContainer, const char *directory);
+static char *_overlayFor(const char *dataDirBase, const char *dataDir, const char *databasePath, bool isContainer);
static uint8_t *_readAsset(DatabaseT *db, const char *key, size_t *bytes, bool sdlMemory);
static bool _readChunk(DatabaseT *db, const char *key, int64_t index, uint8_t *buffer, int64_t capacity, int64_t *length);
static bool _resolve(const char *name, TargetT *target);
@@ -540,15 +540,15 @@ static char *_normalise(const char *name) {
// A game's own directory under the data directory, holding its overlay.
-static char *_overlayFor(const char *dataDirBase, const char *dataDir, const char *databasePath, bool isContainer, const char *directory) {
+static char *_overlayFor(const char *dataDirBase, const char *dataDir, const char *databasePath, bool isContainer) {
char *stem = NULL;
char *result = NULL;
if (isContainer && (dataDir != NULL)) {
- return utilCreateString("%s%s%c", dataDir, directory, utilGetPathSeparator());
+ return utilCreateString("%s%s%c", dataDir, OVERLAY_DIRECTORY, utilGetPathSeparator());
}
stem = vfsDatabaseStem(utilGetLastPathComponent(databasePath));
- result = utilCreateString("%s%s%c%s%c", dataDirBase ? dataDirBase : "", stem, utilGetPathSeparator(), directory, utilGetPathSeparator());
+ result = utilCreateString("%s%s%c%s%c", dataDirBase ? dataDirBase : "", stem, utilGetPathSeparator(), OVERLAY_DIRECTORY, utilGetPathSeparator());
free(stem);
return result;
@@ -655,7 +655,7 @@ static bool _resolve(const char *name, TargetT *target) {
if (target->db != NULL) {
inner = (norm[i] == 0) ? norm + i : norm + i + 1;
if (target->db->overlay == NULL) {
- target->db->overlay = _overlayFor(_dataDirBase, _dataDir, prefix, false, OVERLAY_DIRECTORY);
+ target->db->overlay = _overlayFor(_dataDirBase, _dataDir, prefix, false);
}
free(prefix);
break;
@@ -824,7 +824,7 @@ void vfsInit(const char *container, const char *dataDirBase, const char *dataDir
utilDie("%s is not a Singe game database.", container);
}
free(_container->overlay);
- _container->overlay = _overlayFor(_dataDirBase, _dataDir, container, true, OVERLAY_DIRECTORY);
+ _container->overlay = _overlayFor(_dataDirBase, _dataDir, container, true);
}
}
@@ -872,14 +872,11 @@ bool vfsIsFilesystem(const char *name) {
}
-// The entries directly under a directory name, from the loose directory, the overlay and the
-// database together, each name once, sorted. Free with vfsListFree.
// Whether a name lives only inside a database: no loose file and no overlay copy shadows it. That
// is the case that has no filesystem path at all: vfsFilePath answers with a name that is not
// there, and a stream is the only way to read it.
bool vfsIsPacked(const char *name) {
TargetT target;
- int64_t size = 0;
bool packed = false;
if (!_resolve(name, &target)) {
@@ -887,13 +884,15 @@ bool vfsIsPacked(const char *name) {
}
packed = (target.db != NULL) && (target.key[0] != 0) &&
!utilFileExists(target.path) && !utilFileExists(target.overlay) &&
- _assetSize(target.db, target.key, &size);
+ _assetExists(target.db, target.key);
_targetFree(&target);
return packed;
}
+// The entries directly under a directory name, from the loose directory, the overlay and the
+// database together, each name once, sorted. Free with vfsListFree.
char **vfsList(const char *name, int32_t *count) {
TargetT target;
ListT list;
diff --git a/src/videoPlayer.c b/src/videoPlayer.c
index caec5c70e..304a96538 100644
--- a/src/videoPlayer.c
+++ b/src/videoPlayer.c
@@ -287,14 +287,11 @@ static int64_t _avioSeek(void *opaque, int64_t offset, int whence);
static void _buildFrameTable(VideoPlayerT *v, const char *filename, const char *indexPath);
static int _compareFrames(const void *a, const void *b); // qsort callback. Not changing int.
static void _convertFrame(VideoPlayerT *v, FrameBufferT *buffer, const AVFrame *frame);
-static const AVFrame *_deinterlace(VideoPlayerT *v, AVFrame *frame);
-static void _deinterlaceClose(VideoPlayerT *v);
-static bool _srtAppend(char **text, size_t *used, size_t *room, const char *addition);
-static char *_srtFromRect(const AVSubtitleRect *rect);
-static void _srtTime(int64_t milliseconds, char *out, size_t size);
-static bool _deinterlaceOpen(VideoPlayerT *v, const AVFrame *frame);
static DecodeResultE _decodeFrame(VideoPlayerT *v, int64_t want);
static void _decodeHere(VideoPlayerT *v);
+static const AVFrame *_deinterlace(VideoPlayerT *v, AVFrame *frame);
+static void _deinterlaceClose(VideoPlayerT *v);
+static bool _deinterlaceOpen(VideoPlayerT *v, const AVFrame *frame);
static int _decoderThread(void *data); // SDL thread entry. Not changing int.
static void _feedAudio(VideoPlayerT *v);
static int64_t _findFrameIndex(VideoPlayerT *v, int64_t pts);
@@ -314,13 +311,16 @@ static bool _readIndexCache(VideoPlayerT *v, const char *indexName, co
static void _reportKeyframes(VideoPlayerT *v, const char *filename);
static void _requestFrame(VideoPlayerT *v);
static void _resetClock(VideoPlayerT *v, uint64_t now);
+static const AVCodec *_rkmppDecoder(const AVCodec *decoder);
static void _seekVideo(VideoPlayerT *v, int64_t keyframe);
static enum AVPixelFormat _selectPixelFormat(AVCodecContext *codec, const enum AVPixelFormat *formats); // libavcodec callback.
+static bool _srtAppend(char **text, size_t *used, size_t *room, const char *addition);
+static char *_srtFromRect(const AVSubtitleRect *rect);
+static void _srtTime(int64_t milliseconds, char *out, size_t size);
static int64_t _streamTimeToMs(int64_t ts, AVRational timeBase);
static bool _takeDecodedFrame(VideoPlayerT *v);
static void _trackMixed(void *udata, MIX_Track *track, const SDL_AudioSpec *spec, float *pcm, int32_t samples);
static void _uploadFrame(VideoPlayerT *v);
-static const AVCodec *_rkmppDecoder(const AVCodec *decoder);
static const AVCodec *_v4l2Decoder(const AVCodec *decoder);
static void _writeIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *header);
@@ -835,91 +835,6 @@ static void _convertFrame(VideoPlayerT *v, FrameBufferT *buffer, const AVFrame *
}
-// A frame ready to convert: the deinterlaced one when the picture is interlaced and the option
-// allows it, and the frame itself otherwise. Laserdisc rips are the reason this exists -- a disc
-// held interlaced fields, and a rip that kept them combs on every progressive display.
-static const AVFrame *_deinterlace(VideoPlayerT *v, AVFrame *frame) {
- if ((v->deinterlace == DEINTERLACE_OFF) || (frame->format == AV_PIX_FMT_NONE)) {
- return frame;
- }
- // bwdif is told to leave progressive frames alone, so in automatic mode the graph is built only
- // once something interlaced actually turns up and costs nothing on a progressive disc.
- if ((v->deinterlace == DEINTERLACE_AUTO) && ((frame->flags & AV_FRAME_FLAG_INTERLACED) == 0) && (v->filterGraph == NULL)) {
- return frame;
- }
- if ((v->filterGraph != NULL) && ((frame->format != v->filterFormat) || (frame->width != v->filterWidth) || (frame->height != v->filterHeight))) {
- _deinterlaceClose(v);
- }
- if ((v->filterGraph == NULL) && !_deinterlaceOpen(v, frame)) {
- return frame;
- }
- av_frame_unref(v->filterFrame);
- if (av_buffersrc_add_frame_flags(v->filterSource, frame, AV_BUFFERSRC_FLAG_KEEP_REF) < 0) {
- return frame;
- }
- if (av_buffersink_get_frame(v->filterSink, v->filterFrame) < 0) {
- return frame;
- }
-
- return v->filterFrame;
-}
-
-
-static void _deinterlaceClose(VideoPlayerT *v) {
- if (v->filterGraph != NULL) {
- avfilter_graph_free(&v->filterGraph);
- v->filterSource = NULL;
- v->filterSink = NULL;
- }
-}
-
-
-// buffer -> bwdif -> buffersink. bwdif runs in send_frame mode, one picture out for one in: a
-// disc is addressed by frame number, and a filter that turned each frame into two fields would
-// move every frame in the index.
-static bool _deinterlaceOpen(VideoPlayerT *v, const AVFrame *frame) {
- char arguments[FILTER_ARGUMENTS_MAX];
- const AVFilter *source = avfilter_get_by_name("buffer");
- const AVFilter *sink = avfilter_get_by_name("buffersink");
- const AVFilter *bwdif = avfilter_get_by_name("bwdif");
- AVFilterContext *filter = NULL;
- AVRational ratio = (frame->sample_aspect_ratio.num > 0) ? frame->sample_aspect_ratio : (AVRational){ 1, 1 };
-
- if ((source == NULL) || (sink == NULL) || (bwdif == NULL)) {
- utilTrace("Video %d: this build has no deinterlacer.", v->id);
- v->deinterlace = DEINTERLACE_OFF;
- return false;
- }
- v->filterGraph = avfilter_graph_alloc();
- if (v->filterGraph == NULL) {
- utilDie("Unable to allocate the deinterlacing graph.");
- }
- snprintf(arguments, sizeof(arguments), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
- frame->width, frame->height, frame->format, v->videoTimeBase.num, v->videoTimeBase.den, ratio.num, ratio.den);
- if ((avfilter_graph_create_filter(&v->filterSource, source, "in", arguments, NULL, v->filterGraph) < 0) ||
- (avfilter_graph_create_filter(&filter, bwdif, "bwdif", (v->deinterlace == DEINTERLACE_ON) ? "mode=send_frame:parity=auto:deint=all" : "mode=send_frame:parity=auto:deint=interlaced", NULL, v->filterGraph) < 0) ||
- (avfilter_graph_create_filter(&v->filterSink, sink, "out", NULL, NULL, v->filterGraph) < 0) ||
- (avfilter_link(v->filterSource, 0, filter, 0) < 0) ||
- (avfilter_link(filter, 0, v->filterSink, 0) < 0) ||
- (avfilter_graph_config(v->filterGraph, NULL) < 0)) {
- utilTrace("Video %d: the deinterlacing graph would not build; leaving the picture alone.", v->id);
- _deinterlaceClose(v);
- v->deinterlace = DEINTERLACE_OFF;
- return false;
- }
- v->filterFormat = (enum AVPixelFormat)frame->format;
- v->filterWidth = frame->width;
- v->filterHeight = frame->height;
- if (!v->filterReported) {
- v->filterReported = true;
- utilTrace("Video %d: deinterlacing with bwdif.", v->id);
- }
-
- return true;
-}
-
-
-
// Decodes frame "want" into the back buffer. Runs on the decoder thread. On DECODE_ERROR the
// message is in threadErrMsg; the caller raises threadError under the lock, which publishes both.
static DecodeResultE _decodeFrame(VideoPlayerT *v, int64_t want) {
@@ -1084,6 +999,91 @@ static int _decoderThread(void *data) {
}
+// A frame ready to convert: the deinterlaced one when the picture is interlaced and the option
+// allows it, and the frame itself otherwise. Laserdisc rips are the reason this exists -- a disc
+// held interlaced fields, and a rip that kept them combs on every progressive display.
+static const AVFrame *_deinterlace(VideoPlayerT *v, AVFrame *frame) {
+ if ((v->deinterlace == DEINTERLACE_OFF) || (frame->format == AV_PIX_FMT_NONE)) {
+ return frame;
+ }
+ // bwdif is told to leave progressive frames alone, so in automatic mode the graph is built only
+ // once something interlaced actually turns up and costs nothing on a progressive disc.
+ if ((v->deinterlace == DEINTERLACE_AUTO) && ((frame->flags & AV_FRAME_FLAG_INTERLACED) == 0) && (v->filterGraph == NULL)) {
+ return frame;
+ }
+ if ((v->filterGraph != NULL) && ((frame->format != v->filterFormat) || (frame->width != v->filterWidth) || (frame->height != v->filterHeight))) {
+ _deinterlaceClose(v);
+ }
+ if ((v->filterGraph == NULL) && !_deinterlaceOpen(v, frame)) {
+ return frame;
+ }
+ av_frame_unref(v->filterFrame);
+ if (av_buffersrc_add_frame_flags(v->filterSource, frame, AV_BUFFERSRC_FLAG_KEEP_REF) < 0) {
+ return frame;
+ }
+ if (av_buffersink_get_frame(v->filterSink, v->filterFrame) < 0) {
+ return frame;
+ }
+
+ return v->filterFrame;
+}
+
+
+static void _deinterlaceClose(VideoPlayerT *v) {
+ if (v->filterGraph != NULL) {
+ avfilter_graph_free(&v->filterGraph);
+ v->filterSource = NULL;
+ v->filterSink = NULL;
+ }
+}
+
+
+// buffer -> bwdif -> buffersink. bwdif runs in send_frame mode, one picture out for one in: a
+// disc is addressed by frame number, and a filter that turned each frame into two fields would
+// move every frame in the index.
+static bool _deinterlaceOpen(VideoPlayerT *v, const AVFrame *frame) {
+ char arguments[FILTER_ARGUMENTS_MAX];
+ const AVFilter *source = avfilter_get_by_name("buffer");
+ const AVFilter *sink = avfilter_get_by_name("buffersink");
+ const AVFilter *bwdif = avfilter_get_by_name("bwdif");
+ AVFilterContext *filter = NULL;
+ AVRational ratio = (frame->sample_aspect_ratio.num > 0) ? frame->sample_aspect_ratio : (AVRational){ 1, 1 };
+
+ if ((source == NULL) || (sink == NULL) || (bwdif == NULL)) {
+ utilTrace("Video %d: this build has no deinterlacer.", v->id);
+ v->deinterlace = DEINTERLACE_OFF;
+ return false;
+ }
+ v->filterGraph = avfilter_graph_alloc();
+ if (v->filterGraph == NULL) {
+ utilDie("Unable to allocate the deinterlacing graph.");
+ }
+ snprintf(arguments, sizeof(arguments), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
+ frame->width, frame->height, frame->format, v->videoTimeBase.num, v->videoTimeBase.den, ratio.num, ratio.den);
+ if ((avfilter_graph_create_filter(&v->filterSource, source, "in", arguments, NULL, v->filterGraph) < 0) ||
+ (avfilter_graph_create_filter(&filter, bwdif, "bwdif", (v->deinterlace == DEINTERLACE_ON) ? "mode=send_frame:parity=auto:deint=all" : "mode=send_frame:parity=auto:deint=interlaced", NULL, v->filterGraph) < 0) ||
+ (avfilter_graph_create_filter(&v->filterSink, sink, "out", NULL, NULL, v->filterGraph) < 0) ||
+ (avfilter_link(v->filterSource, 0, filter, 0) < 0) ||
+ (avfilter_link(filter, 0, v->filterSink, 0) < 0) ||
+ (avfilter_graph_config(v->filterGraph, NULL) < 0)) {
+ utilTrace("Video %d: the deinterlacing graph would not build; leaving the picture alone.", v->id);
+ _deinterlaceClose(v);
+ v->deinterlace = DEINTERLACE_OFF;
+ return false;
+ }
+ v->filterFormat = (enum AVPixelFormat)frame->format;
+ v->filterWidth = frame->width;
+ v->filterHeight = frame->height;
+ if (!v->filterReported) {
+ v->filterReported = true;
+ utilTrace("Video %d: deinterlacing with bwdif.", v->id);
+ }
+
+ return true;
+}
+
+
+
// Keeps the mixer track's stream topped up from the current audio track.
static void _feedAudio(VideoPlayerT *v) {
int32_t result = 0;
@@ -1537,6 +1537,74 @@ static void _resetClock(VideoPlayerT *v, uint64_t now) {
}
+// The V4L2 memory-to-memory decoder for the stream's codec on a Pi build, NULL when there is none or
+// hardware decoding is off. Whether the device exists is found out when the decoder opens.
+// Rockchip boards decode through the vendor's Media Process Platform, which FFmpeg exposes as named
+// decoders in the same way as V4L2's. They exist only when FFmpeg was built against that library,
+// which is why this is compiled out everywhere else. Tried before V4L2: a Rockchip running its
+// vendor kernel has both, and the MPP path is the one that works there.
+static const AVCodec *_rkmppDecoder(const AVCodec *decoder) {
+ static const V4l2DecoderT table[] = {
+ { AV_CODEC_ID_H264, "h264_rkmpp" },
+ { AV_CODEC_ID_HEVC, "hevc_rkmpp" }
+ };
+ size_t x = 0;
+
+ if (!RKMPP_DECODE || !_hardwareDecoding) {
+ return NULL;
+ }
+ for (x = 0; x < SDL_arraysize(table); x++) {
+ if (table[x].id == decoder->id) {
+ return avcodec_find_decoder_by_name(table[x].name);
+ }
+ }
+
+ return NULL;
+}
+
+
+static void _seekVideo(VideoPlayerT *v, int64_t keyframe) {
+ if (av_seek_frame(v->videoFormat, v->videoStream, v->frames[keyframe].pts, AVSEEK_FLAG_BACKWARD) < 0) {
+ avformat_seek_file(v->videoFormat, v->videoStream, INT64_MIN, 0, INT64_MAX, 0);
+ keyframe = 0;
+ }
+ avcodec_flush_buffers(v->videoCodec);
+ if (v->packetPending) {
+ av_packet_unref(v->videoPacket);
+ v->packetPending = false;
+ }
+ // Streams without timestamps are counted from here; the rest are matched by timestamp.
+ v->nextDecodeFrame = keyframe;
+ v->seekKeyframe = keyframe;
+ v->videoDrained = false;
+}
+
+
+// libavcodec asks which of the formats it can produce we want; take the hardware one when offered.
+// The list is every hardware format the build knows followed by the software one, so the fallback
+// is the first entry that is not a hardware surface, not the first entry.
+static enum AVPixelFormat _selectPixelFormat(AVCodecContext *codec, const enum AVPixelFormat *formats) {
+ VideoPlayerT *v = (VideoPlayerT *)codec->opaque;
+ const AVPixFmtDescriptor *desc = NULL;
+ int32_t x = 0;
+
+ for (x = 0; formats[x] != AV_PIX_FMT_NONE; x++) {
+ if (formats[x] == v->hwPixelFormat) {
+ return formats[x];
+ }
+ }
+ utilTrace("Video %d: hardware decoder declined the stream; decoding in software.", v->id);
+ for (x = 0; formats[x] != AV_PIX_FMT_NONE; x++) {
+ desc = av_pix_fmt_desc_get(formats[x]);
+ if ((desc != NULL) && !(desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
+ return formats[x];
+ }
+ }
+
+ return formats[0];
+}
+
+
// Positions the video demuxer at a keyframe and resets the decoder. Only ever called by whichever
// thread is decoding: the decoder thread, or the frame loop in deterministic mode.
// Appends to a growing string, doubling it as it goes. False when it will not grow.
@@ -1644,48 +1712,6 @@ static void _srtTime(int64_t milliseconds, char *out, size_t size) {
}
-static void _seekVideo(VideoPlayerT *v, int64_t keyframe) {
- if (av_seek_frame(v->videoFormat, v->videoStream, v->frames[keyframe].pts, AVSEEK_FLAG_BACKWARD) < 0) {
- avformat_seek_file(v->videoFormat, v->videoStream, INT64_MIN, 0, INT64_MAX, 0);
- keyframe = 0;
- }
- avcodec_flush_buffers(v->videoCodec);
- if (v->packetPending) {
- av_packet_unref(v->videoPacket);
- v->packetPending = false;
- }
- // Streams without timestamps are counted from here; the rest are matched by timestamp.
- v->nextDecodeFrame = keyframe;
- v->seekKeyframe = keyframe;
- v->videoDrained = false;
-}
-
-
-// libavcodec asks which of the formats it can produce we want; take the hardware one when offered.
-// The list is every hardware format the build knows followed by the software one, so the fallback
-// is the first entry that is not a hardware surface, not the first entry.
-static enum AVPixelFormat _selectPixelFormat(AVCodecContext *codec, const enum AVPixelFormat *formats) {
- VideoPlayerT *v = (VideoPlayerT *)codec->opaque;
- const AVPixFmtDescriptor *desc = NULL;
- int32_t x = 0;
-
- for (x = 0; formats[x] != AV_PIX_FMT_NONE; x++) {
- if (formats[x] == v->hwPixelFormat) {
- return formats[x];
- }
- }
- utilTrace("Video %d: hardware decoder declined the stream; decoding in software.", v->id);
- for (x = 0; formats[x] != AV_PIX_FMT_NONE; x++) {
- desc = av_pix_fmt_desc_get(formats[x]);
- if ((desc != NULL) && !(desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
- return formats[x];
- }
- }
-
- return formats[0];
-}
-
-
static int64_t _streamTimeToMs(int64_t ts, AVRational timeBase) {
return av_rescale(ts, (int64_t)timeBase.num * (int64_t)MS_PER_SECOND, timeBase.den);
}
@@ -1762,32 +1788,6 @@ static void _uploadFrame(VideoPlayerT *v) {
}
-// The V4L2 memory-to-memory decoder for the stream's codec on a Pi build, NULL when there is none or
-// hardware decoding is off. Whether the device exists is found out when the decoder opens.
-// Rockchip boards decode through the vendor's Media Process Platform, which FFmpeg exposes as named
-// decoders in the same way as V4L2's. They exist only when FFmpeg was built against that library,
-// which is why this is compiled out everywhere else. Tried before V4L2: a Rockchip running its
-// vendor kernel has both, and the MPP path is the one that works there.
-static const AVCodec *_rkmppDecoder(const AVCodec *decoder) {
- static const V4l2DecoderT table[] = {
- { AV_CODEC_ID_H264, "h264_rkmpp" },
- { AV_CODEC_ID_HEVC, "hevc_rkmpp" }
- };
- size_t x = 0;
-
- if (!RKMPP_DECODE || !_hardwareDecoding) {
- return NULL;
- }
- for (x = 0; x < SDL_arraysize(table); x++) {
- if (table[x].id == decoder->id) {
- return avcodec_find_decoder_by_name(table[x].name);
- }
- }
-
- return NULL;
-}
-
-
static const AVCodec *_v4l2Decoder(const AVCodec *decoder) {
static const V4l2DecoderT table[] = {
{ AV_CODEC_ID_H263, "h263_v4l2m2m" },
@@ -1881,6 +1881,38 @@ int32_t videoGetAudioTracks(int32_t playerHandle) {
}
+// What the decoder will be, for the trace header: the platform's hardware decoders in the order
+// they are tried, or software when there are none or --softwarevideo ruled them out. Which one a
+// given video actually got is traced as that video opens, since a codec may offer neither.
+const char *videoGetDecoderDescription(void) {
+ if (!_hardwareDecoding) {
+ return "software only (--softwarevideo)";
+ }
+ if (V4L2_DECODE) {
+ // An ARM build may carry three: the stateless V4L2 decoders, Rockchip's own, and the
+ // stateful V4L2 ones. Which of them a board actually has is a question for the board.
+ if (RKMPP_DECODE && V4L2_REQUEST_DECODE) {
+ return "hardware v4l2 stateless, rkmpp or v4l2m2m where the board allows, software otherwise";
+ }
+ if (V4L2_REQUEST_DECODE) {
+ return "hardware v4l2 stateless or v4l2m2m where the board allows, software otherwise";
+ }
+ if (RKMPP_DECODE) {
+ return "hardware rkmpp or v4l2m2m where the board allows, software otherwise";
+ }
+
+ return "hardware v4l2m2m where the codec allows, software otherwise";
+ }
+#if defined(_WIN32)
+ return "hardware d3d11va or dxva2 where the codec allows, software otherwise";
+#elif defined(__APPLE__)
+ return "hardware videotoolbox where the codec allows, software otherwise";
+#else
+ return "hardware vaapi or vdpau where the codec allows, software otherwise";
+#endif
+}
+
+
// Frames per second of the loaded video, which is what a subtitle's timestamps are turned into
// frame numbers with.
double videoGetFps(int32_t playerHandle) {
@@ -1890,178 +1922,6 @@ double videoGetFps(int32_t playerHandle) {
}
-// How many subtitle tracks the file holds, bitmap ones included: they are counted so that the
-// numbering matches what a player or ffprobe shows, and videoReadSubtitles says which have words.
-int32_t videoGetSubtitleTracks(int32_t playerHandle) {
- VideoPlayerT *v = _getPlayer(playerHandle, "videoGetSubtitleTracks");
- AVFormatContext *format = _formatOpen(v->filename);
- int32_t count = 0;
- uint32_t x = 0;
-
- if (format == NULL) {
- return 0;
- }
- if (avformat_find_stream_info(format, NULL) >= 0) {
- for (x = 0; x < format->nb_streams; x++) {
- if (format->streams[x]->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
- count++;
- }
- }
- }
- _formatClose(&format);
-
- return count;
-}
-
-
-// The language of a subtitle track as the file labels it, "" when it says nothing. The pointer is
-// good until the next call.
-const char *videoGetSubtitleLanguage(int32_t playerHandle, int32_t track) {
- static char language[LANGUAGE_CODE_BYTES];
- VideoPlayerT *v = _getPlayer(playerHandle, "videoGetSubtitleLanguage");
- AVFormatContext *format = _formatOpen(v->filename);
- AVDictionaryEntry *entry = NULL;
- int32_t seen = 0;
- uint32_t x = 0;
-
- language[0] = '\0';
- if (format == NULL) {
- return language;
- }
- if (avformat_find_stream_info(format, NULL) >= 0) {
- for (x = 0; x < format->nb_streams; x++) {
- if (format->streams[x]->codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
- continue;
- }
- if (seen == track) {
- entry = av_dict_get(format->streams[x]->metadata, "language", NULL, 0);
- if ((entry != NULL) && (entry->value != NULL)) {
- SDL_strlcpy(language, entry->value, sizeof(language));
- }
- break;
- }
- seen++;
- }
- }
- _formatClose(&format);
-
- return language;
-}
-
-
-// One subtitle track read out of the container as SubRip text, which is the form the engine's own
-// subtitle loader already takes. The whole file is walked once, decoding nothing but the subtitle
-// packets, so this costs a pass over the file and no picture decoding at all. NULL when the track
-// does not exist, holds pictures rather than words, or yields nothing; free() what comes back.
-char *videoReadSubtitles(int32_t playerHandle, int32_t track) {
- VideoPlayerT *v = _getPlayer(playerHandle, "videoReadSubtitles");
- AVFormatContext *format = _formatOpen(v->filename);
- AVCodecContext *codec = NULL;
- const AVCodec *decoder = NULL;
- AVPacket *packet = NULL;
- AVSubtitle subtitle;
- AVRational timeBase;
- char *text = NULL;
- char *words = NULL;
- char line[SRT_CUE_MAX];
- char from[SRT_TIME_MAX];
- char to[SRT_TIME_MAX];
- size_t used = 0;
- size_t room = 0;
- int64_t start = 0;
- int64_t end = 0;
- int32_t stream = -1;
- int32_t seen = 0;
- int32_t cues = 0;
- int got = 0;
- uint32_t x = 0;
- bool ok = true;
-
- if (format == NULL) {
- return NULL;
- }
- if (avformat_find_stream_info(format, NULL) < 0) {
- _formatClose(&format);
- return NULL;
- }
- for (x = 0; x < format->nb_streams; x++) {
- if (format->streams[x]->codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
- continue;
- }
- if (seen == track) {
- stream = (int32_t)x;
- break;
- }
- seen++;
- }
- if (stream < 0) {
- _formatClose(&format);
- return NULL;
- }
- timeBase = format->streams[stream]->time_base;
- decoder = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id);
- codec = (decoder != NULL) ? avcodec_alloc_context3(decoder) : NULL;
- packet = av_packet_alloc();
- if ((codec == NULL) || (packet == NULL) ||
- (avcodec_parameters_to_context(codec, format->streams[stream]->codecpar) < 0) ||
- (avcodec_open2(codec, decoder, NULL) < 0)) {
- utilTrace("Video %d: subtitle track %d has no decoder in this build.", v->id, track);
- av_packet_free(&packet);
- avcodec_free_context(&codec);
- _formatClose(&format);
- return NULL;
- }
- while (ok && (av_read_frame(format, packet) >= 0)) {
- if (packet->stream_index != stream) {
- av_packet_unref(packet);
- continue;
- }
- memset(&subtitle, 0, sizeof(subtitle));
- got = 0;
- if (avcodec_decode_subtitle2(codec, &subtitle, &got, packet) >= 0) {
- if (got != 0) {
- start = (int64_t)((double)packet->pts * av_q2d(timeBase) * MS_PER_SECOND) + (int64_t)subtitle.start_display_time;
- if (subtitle.end_display_time > subtitle.start_display_time) {
- end = start + (int64_t)(subtitle.end_display_time - subtitle.start_display_time);
- } else {
- end = start + (int64_t)((double)packet->duration * av_q2d(timeBase) * MS_PER_SECOND);
- }
- for (x = 0; (x < subtitle.num_rects) && ok; x++) {
- words = _srtFromRect(subtitle.rects[x]);
- if (words == NULL) {
- continue;
- }
- cues++;
- _srtTime(start, from, sizeof(from));
- _srtTime(end, to, sizeof(to));
- snprintf(line, sizeof(line), "%d\n", cues);
- ok = _srtAppend(&text, &used, &room, line) &&
- _srtAppend(&text, &used, &room, from) &&
- _srtAppend(&text, &used, &room, " --> ") &&
- _srtAppend(&text, &used, &room, to) &&
- _srtAppend(&text, &used, &room, "\n") &&
- _srtAppend(&text, &used, &room, words) &&
- _srtAppend(&text, &used, &room, "\n\n");
- free(words);
- }
- }
- avsubtitle_free(&subtitle);
- }
- av_packet_unref(packet);
- }
- av_packet_free(&packet);
- avcodec_free_context(&codec);
- _formatClose(&format);
- if (!ok || (cues == 0)) {
- free(text);
- return NULL;
- }
- utilTrace("Video %d: subtitle track %d read as %d cues.", v->id, track, cues);
-
- return text;
-}
-
-
int64_t videoGetFrame(int32_t playerHandle) {
return _getPlayer(playerHandle, "videoGetFrame")->frame;
}
@@ -2114,38 +1974,6 @@ MIX_Mixer *videoGetMixer(void) {
}
-// What the decoder will be, for the trace header: the platform's hardware decoders in the order
-// they are tried, or software when there are none or --softwarevideo ruled them out. Which one a
-// given video actually got is traced as that video opens, since a codec may offer neither.
-const char *videoGetDecoderDescription(void) {
- if (!_hardwareDecoding) {
- return "software only (--softwarevideo)";
- }
- if (V4L2_DECODE) {
- // An ARM build may carry three: the stateless V4L2 decoders, Rockchip's own, and the
- // stateful V4L2 ones. Which of them a board actually has is a question for the board.
- if (RKMPP_DECODE && V4L2_REQUEST_DECODE) {
- return "hardware v4l2 stateless, rkmpp or v4l2m2m where the board allows, software otherwise";
- }
- if (V4L2_REQUEST_DECODE) {
- return "hardware v4l2 stateless or v4l2m2m where the board allows, software otherwise";
- }
- if (RKMPP_DECODE) {
- return "hardware rkmpp or v4l2m2m where the board allows, software otherwise";
- }
-
- return "hardware v4l2m2m where the codec allows, software otherwise";
- }
-#if defined(_WIN32)
- return "hardware d3d11va or dxva2 where the codec allows, software otherwise";
-#elif defined(__APPLE__)
- return "hardware videotoolbox where the codec allows, software otherwise";
-#else
- return "hardware vaapi or vdpau where the codec allows, software otherwise";
-#endif
-}
-
-
// Reads one pixel of the frame being shown. Returns false if there is no frame yet.
bool videoGetPixel(int32_t playerHandle, int32_t x, int32_t y, uint8_t *r, uint8_t *g, uint8_t *b) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetPixel");
@@ -2200,6 +2028,65 @@ bool videoGetPixels(int32_t playerHandle, const uint8_t **pixels, int32_t *pitch
}
+// The language of a subtitle track as the file labels it, "" when it says nothing. The pointer is
+// good until the next call.
+const char *videoGetSubtitleLanguage(int32_t playerHandle, int32_t track) {
+ static char language[LANGUAGE_CODE_BYTES];
+ VideoPlayerT *v = _getPlayer(playerHandle, "videoGetSubtitleLanguage");
+ AVFormatContext *format = _formatOpen(v->filename);
+ AVDictionaryEntry *entry = NULL;
+ int32_t seen = 0;
+ uint32_t x = 0;
+
+ language[0] = '\0';
+ if (format == NULL) {
+ return language;
+ }
+ if (avformat_find_stream_info(format, NULL) >= 0) {
+ for (x = 0; x < format->nb_streams; x++) {
+ if (format->streams[x]->codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
+ continue;
+ }
+ if (seen == track) {
+ entry = av_dict_get(format->streams[x]->metadata, "language", NULL, 0);
+ if ((entry != NULL) && (entry->value != NULL)) {
+ SDL_strlcpy(language, entry->value, sizeof(language));
+ }
+ break;
+ }
+ seen++;
+ }
+ }
+ _formatClose(&format);
+
+ return language;
+}
+
+
+// How many subtitle tracks the file holds, bitmap ones included: they are counted so that the
+// numbering matches what a player or ffprobe shows, and videoReadSubtitles says which have words.
+int32_t videoGetSubtitleTracks(int32_t playerHandle) {
+ VideoPlayerT *v = _getPlayer(playerHandle, "videoGetSubtitleTracks");
+ AVFormatContext *format = _formatOpen(v->filename);
+ int32_t count = 0;
+ uint32_t x = 0;
+
+ if (format == NULL) {
+ return 0;
+ }
+ if (avformat_find_stream_info(format, NULL) >= 0) {
+ for (x = 0; x < format->nb_streams; x++) {
+ if (format->streams[x]->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
+ count++;
+ }
+ }
+ }
+ _formatClose(&format);
+
+ return count;
+}
+
+
void videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t *rightPercent) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetVolume");
@@ -2397,6 +2284,119 @@ void videoQuit(void) {
}
+// One subtitle track read out of the container as SubRip text, which is the form the engine's own
+// subtitle loader already takes. The whole file is walked once, decoding nothing but the subtitle
+// packets, so this costs a pass over the file and no picture decoding at all. NULL when the track
+// does not exist, holds pictures rather than words, or yields nothing; free() what comes back.
+char *videoReadSubtitles(int32_t playerHandle, int32_t track) {
+ VideoPlayerT *v = _getPlayer(playerHandle, "videoReadSubtitles");
+ AVFormatContext *format = _formatOpen(v->filename);
+ AVCodecContext *codec = NULL;
+ const AVCodec *decoder = NULL;
+ AVPacket *packet = NULL;
+ AVSubtitle subtitle;
+ AVRational timeBase;
+ char *text = NULL;
+ char *words = NULL;
+ char line[SRT_CUE_MAX];
+ char from[SRT_TIME_MAX];
+ char to[SRT_TIME_MAX];
+ size_t used = 0;
+ size_t room = 0;
+ int64_t start = 0;
+ int64_t end = 0;
+ int32_t stream = -1;
+ int32_t seen = 0;
+ int32_t cues = 0;
+ int got = 0;
+ uint32_t x = 0;
+ bool ok = true;
+
+ if (format == NULL) {
+ return NULL;
+ }
+ if (avformat_find_stream_info(format, NULL) < 0) {
+ _formatClose(&format);
+ return NULL;
+ }
+ for (x = 0; x < format->nb_streams; x++) {
+ if (format->streams[x]->codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
+ continue;
+ }
+ if (seen == track) {
+ stream = (int32_t)x;
+ break;
+ }
+ seen++;
+ }
+ if (stream < 0) {
+ _formatClose(&format);
+ return NULL;
+ }
+ timeBase = format->streams[stream]->time_base;
+ decoder = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id);
+ codec = (decoder != NULL) ? avcodec_alloc_context3(decoder) : NULL;
+ packet = av_packet_alloc();
+ if ((codec == NULL) || (packet == NULL) ||
+ (avcodec_parameters_to_context(codec, format->streams[stream]->codecpar) < 0) ||
+ (avcodec_open2(codec, decoder, NULL) < 0)) {
+ utilTrace("Video %d: subtitle track %d has no decoder in this build.", v->id, track);
+ av_packet_free(&packet);
+ avcodec_free_context(&codec);
+ _formatClose(&format);
+ return NULL;
+ }
+ while (ok && (av_read_frame(format, packet) >= 0)) {
+ if (packet->stream_index != stream) {
+ av_packet_unref(packet);
+ continue;
+ }
+ memset(&subtitle, 0, sizeof(subtitle));
+ got = 0;
+ if (avcodec_decode_subtitle2(codec, &subtitle, &got, packet) >= 0) {
+ if (got != 0) {
+ start = (int64_t)((double)packet->pts * av_q2d(timeBase) * MS_PER_SECOND) + (int64_t)subtitle.start_display_time;
+ if (subtitle.end_display_time > subtitle.start_display_time) {
+ end = start + (int64_t)(subtitle.end_display_time - subtitle.start_display_time);
+ } else {
+ end = start + (int64_t)((double)packet->duration * av_q2d(timeBase) * MS_PER_SECOND);
+ }
+ for (x = 0; (x < subtitle.num_rects) && ok; x++) {
+ words = _srtFromRect(subtitle.rects[x]);
+ if (words == NULL) {
+ continue;
+ }
+ cues++;
+ _srtTime(start, from, sizeof(from));
+ _srtTime(end, to, sizeof(to));
+ snprintf(line, sizeof(line), "%d\n", cues);
+ ok = _srtAppend(&text, &used, &room, line) &&
+ _srtAppend(&text, &used, &room, from) &&
+ _srtAppend(&text, &used, &room, " --> ") &&
+ _srtAppend(&text, &used, &room, to) &&
+ _srtAppend(&text, &used, &room, "\n") &&
+ _srtAppend(&text, &used, &room, words) &&
+ _srtAppend(&text, &used, &room, "\n\n");
+ free(words);
+ }
+ }
+ avsubtitle_free(&subtitle);
+ }
+ av_packet_unref(packet);
+ }
+ av_packet_free(&packet);
+ avcodec_free_context(&codec);
+ _formatClose(&format);
+ if (!ok || (cues == 0)) {
+ free(text);
+ return NULL;
+ }
+ utilTrace("Video %d: subtitle track %d read as %d cues.", v->id, track, cues);
+
+ return text;
+}
+
+
// Replaces the audio side with another file (NULL: the video's own audio, or silence when it has
// none) and realigns it at the current frame, keeping the volume, the play state and the track
// index when the new file has it. False, with the old audio untouched, when the file cannot be read.
@@ -2485,12 +2485,6 @@ void videoSetBlend(int32_t playerHandle, bool enabled) {
}
-// Chosen before any video loads; existing players keep whatever they opened with.
-void videoSetHardwareDecoding(bool enabled) {
- _hardwareDecoding = enabled;
-}
-
-
// Brightens or darkens the picture. The level runs 0 to LUMA_LEVEL_MAX with LUMA_LEVEL_NEUTRAL
// changing nothing, and the distance from neutral is applied to every luma sample in eighths, so 0
// halves the brightness and 8 raises it by half. Turning it off, or asking for the neutral level,
@@ -2500,6 +2494,12 @@ void videoSetDeinterlace(DeinterlaceE mode) {
}
+// Chosen before any video loads; existing players keep whatever they opened with.
+void videoSetHardwareDecoding(bool enabled) {
+ _hardwareDecoding = enabled;
+}
+
+
void videoSetLuma(int32_t playerHandle, bool enabled, int32_t level) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoSetLuma");
diff --git a/testScripts/author/platformer.game b/testScripts/author/platformer.game
index bb1d231d3..607809e8e 100644
--- a/testScripts/author/platformer.game
+++ b/testScripts/author/platformer.game
@@ -1,4 +1,4 @@
--- A game description: data, not code. Singe/AuthorCompile.singe turns it into a Singe game.
+-- A game description: data, not code. Forge/AuthorCompile.singe turns it into a Singe game.
--
-- Genre one of two. This one uses a single world2d layer and shares nothing with the QTE beside
-- it -- no disc, no branching, no time windows -- which is the point: if the core can express
diff --git a/testScripts/packedIo/packedIo.singe b/testScripts/packedIo/packedIo.singe
index bb7baade3..ac7dc605c 100644
--- a/testScripts/packedIo/packedIo.singe
+++ b/testScripts/packedIo/packedIo.singe
@@ -63,6 +63,26 @@ check("f:lines count", count, 3)
check("f:lines leaves it open", io.type(g), "file")
g:close()
+-- More than one format a step keeps every value, and a later format coming up empty hands back
+-- what came before it with nil in its place, as Lua does; only an empty first read ends the loop.
+local pairs = {}
+
+for a, b in io.lines("packedIo/numbers.txt", "n", "n") do
+ pairs[#pairs + 1] = { a, b }
+end
+check("lines n n steps", #pairs, 2)
+check("lines n n first", pairs[1][1] + pairs[1][2], 46.5)
+check("lines n n second", pairs[2][1], 16)
+check("lines n n short", pairs[2][2], nil)
+
+-- read(0) is the test for the end of the file.
+local z = io.open("packedIo/tail.txt")
+
+check("read 0 with more", z:read(0), "")
+z:read("a")
+check("read 0 at end", z:read(0), nil)
+z:close()
+
-- Numbers, and the default input.
local n = io.open("packedIo/numbers.txt")
diff --git a/testScripts/scene52.singe b/testScripts/scene52.singe
index f234c90db..06002f6fa 100644
--- a/testScripts/scene52.singe
+++ b/testScripts/scene52.singe
@@ -59,16 +59,16 @@ function onOverlayUpdate()
frames = frames + 1
if not held then
- authorKeyDown(0, SCANCODE.RIGHT)
+ authorKeyDown(0, SCANCODE.RIGHT.value)
held = true
end
-- Jump once, when the hero is under way and has ground beneath it.
if (not jumped) and (hx >= JUMP_AT) and playerIsOnGround(hero.node) then
- authorKeyDown(0, SCANCODE.SPACE)
+ authorKeyDown(0, SCANCODE.SPACE.value)
jumped = true
elseif jumped then
- authorKeyUp(0, SCANCODE.SPACE)
+ authorKeyUp(0, SCANCODE.SPACE.value)
end
local r = gameUpdate()
diff --git a/testScripts/scene55.singe b/testScripts/scene55.singe
index 9bb821210..e825248ee 100644
--- a/testScripts/scene55.singe
+++ b/testScripts/scene55.singe
@@ -77,7 +77,7 @@ local function step()
if after == before then
debugPrint("RULES FAIL the new rule did not reach the compiled game")
- elseif after:find('authorKeyHeld%(SCANCODE%.UP%)') and after:find('authorOnGround%("hero"%)') and after:find('authorJump%("hero"%)') then
+ elseif after:find('authorKeyHeld%(SCANCODE%.UP%.value%)') and after:find('authorOnGround%("hero"%)') and after:find('authorJump%("hero"%)') then
debugPrint("RULES the compiled game carries the new rule")
else
debugPrint("RULES FAIL the compiled game is missing part of the new rule")
@@ -89,7 +89,7 @@ local function step()
forgePartDelete()
local after = compiled()
- if after:find('authorKeyHeld%(SCANCODE%.UP%)') then
+ if after:find('authorKeyHeld%(SCANCODE%.UP%.value%)') then
debugPrint("RULES FAIL the deleted condition is still compiled")
else
debugPrint("RULES the deleted condition is gone from the compiled game")
diff --git a/testScripts/scene57.singe b/testScripts/scene57.singe
new file mode 100644
index 000000000..c9836d792
--- /dev/null
+++ b/testScripts/scene57.singe
@@ -0,0 +1,236 @@
+-- Forge as a game (PLAN section 58): the chooser, the engine callbacks, and the editing that
+-- needs no pointer -- entities added, renamed and deleted, a rule built from the pickers, undo.
+--
+-- Unlike scene54 to 56 this does not set FORGE_LIBRARY: Forge installs its own callbacks, and
+-- the scene drives them the way the engine would, two integers to onKeyPressed. What is
+-- asserted is that every key reaches the description and that a rename reaches the rules.
+dofile("Forge/Forge.singe")
+
+local forgeUpdate = onOverlayUpdate
+local frames = 0
+local stage = 0
+local opened = nil
+
+
+local function key(code, character)
+ onKeyPressed(character or 0, code.value)
+end
+
+
+local function typeIn(text)
+ for c in string.gmatch(text, ".") do
+ key(SCANCODE.A, string.byte(c))
+ end
+end
+
+
+local function step()
+ if stage == 1 then
+ -- The chooser is up and offers a new game at least.
+ if FORGE.game ~= nil then
+ debugPrint("GAME FAIL the editor is open before anything was chosen")
+ end
+ debugPrint("GAME the chooser offers " .. #forgeFiles() .. " entries")
+ -- The last entry is always a new game.
+ for _ = 1, 20 do
+ key(SCANCODE.DOWN)
+ end
+ key(SCANCODE.RETURN)
+ if FORGE.game == nil then
+ debugPrint("GAME FAIL ENTER on the chooser opened nothing")
+ else
+ opened = FORGE.path
+ debugPrint("GAME opened " .. opened .. " with " .. #FORGE.game.entities .. " entities")
+ end
+
+ elseif stage == 2 then
+ -- Add an entity, name it, and give it a look by typing the fields ENTER walks.
+ local count = #FORGE.game.entities
+
+ key(SCANCODE.A)
+ if #FORGE.game.entities ~= count + 1 then
+ debugPrint("GAME FAIL A did not add an entity")
+ end
+ key(SCANCODE.RETURN) -- id
+ typeIn("coin")
+ key(SCANCODE.RETURN) -- x
+ typeIn("300")
+ key(SCANCODE.RETURN) -- y
+ typeIn("200")
+ key(SCANCODE.RETURN) -- kind
+ key(SCANCODE.ESCAPE) -- untouched
+ local coin = FORGE.game.entities[#FORGE.game.entities]
+
+ if coin.id == "coin" and coin.x == 300 and coin.y == 200 and coin.look.kind == "box" then
+ debugPrint("GAME typed an entity in: " .. coin.id .. " at " .. coin.x .. "," .. coin.y)
+ else
+ debugPrint("GAME FAIL the typed entity is " .. tostring(coin.id) .. " at " .. tostring(coin.x) .. "," .. tostring(coin.y) .. " " .. tostring(coin.look.kind))
+ end
+
+ elseif stage == 3 then
+ -- Renaming the hero has to reach every rule that names it.
+ key(SCANCODE.UP)
+ key(SCANCODE.UP)
+ key(SCANCODE.UP)
+ key(SCANCODE.UP)
+ key(SCANCODE.DOWN) -- ground, then hero
+ key(SCANCODE.RETURN)
+ typeIn("player")
+ key(SCANCODE.ESCAPE) -- ESC puts it back: the hero keeps its name
+ if FORGE.game.entities[FORGE.selected].id ~= "hero" then
+ debugPrint("GAME FAIL escape did not put the name back")
+ end
+ key(SCANCODE.RETURN)
+ typeIn("player")
+ key(SCANCODE.RETURN) -- commits, moves on to x
+ key(SCANCODE.ESCAPE)
+ local named = 0
+
+ for _, rule in ipairs(FORGE.game.rules) do
+ for _, item in ipairs(rule.act) do
+ if item.entity == "player" then
+ named = named + 1
+ end
+ end
+ end
+ if FORGE.game.entities[FORGE.selected].id == "player" and named == 3 then
+ debugPrint("GAME renamed the hero and " .. named .. " actions followed")
+ else
+ debugPrint("GAME FAIL rename: id " .. tostring(FORGE.game.entities[FORGE.selected].id) .. ", " .. named .. " actions renamed")
+ end
+
+ elseif stage == 4 then
+ -- A rule from the pickers: N, then C picks the first condition, T the first action.
+ local rules = #FORGE.game.rules
+
+ key(SCANCODE.TAB)
+ key(SCANCODE.N)
+ key(SCANCODE.C)
+ if FORGE.picker == nil then
+ debugPrint("GAME FAIL C opened no picker")
+ end
+ key(SCANCODE.DOWN)
+ key(SCANCODE.RETURN)
+ key(SCANCODE.T)
+ key(SCANCODE.RETURN)
+ local rule = FORGE.game.rules[#FORGE.game.rules]
+
+ if #FORGE.game.rules == rules + 1 and #rule.when == 1 and #rule.act == 1 then
+ debugPrint("GAME built a rule from the pickers: when " .. rule.when[1][1] .. " then " .. rule.act[1][1])
+ else
+ debugPrint("GAME FAIL the picked rule has " .. #(rule.when or {}) .. " conditions and " .. #(rule.act or {}) .. " actions")
+ end
+
+ elseif stage == 5 then
+ -- Undo takes the action back, then the condition, then the rule; redo brings the rule
+ -- back, and a change by hand ends the redo history.
+ local rules = #FORGE.game.rules
+
+ key(SCANCODE.U)
+ key(SCANCODE.U)
+ key(SCANCODE.U)
+ if #FORGE.game.rules == rules - 1 then
+ debugPrint("GAME three undos took the rule back out")
+ else
+ debugPrint("GAME FAIL after three undos there are " .. #FORGE.game.rules .. " rules, not " .. (rules - 1))
+ end
+ key(SCANCODE.R)
+ if (#FORGE.game.rules == rules) and (#FORGE.game.rules[rules].when == 0) then
+ debugPrint("GAME redo brought the empty rule back")
+ else
+ debugPrint("GAME FAIL redo left " .. #FORGE.game.rules .. " rules")
+ end
+ key(SCANCODE.U)
+ key(SCANCODE.N)
+ key(SCANCODE.R)
+ if #FORGE.redo == 0 and #FORGE.game.rules == rules then
+ debugPrint("GAME a new change emptied the redo history")
+ else
+ debugPrint("GAME FAIL redo history survived a change: " .. #FORGE.redo .. " kept")
+ end
+ key(SCANCODE.U)
+
+ elseif stage == 6 then
+ -- Duplicate and delete in the entity list.
+ key(SCANCODE.TAB)
+ local count = #FORGE.game.entities
+
+ key(SCANCODE.D)
+ key(SCANCODE.DELETE)
+ if #FORGE.game.entities == count then
+ debugPrint("GAME duplicate then delete leaves " .. count .. " entities")
+ else
+ debugPrint("GAME FAIL duplicate then delete leaves " .. #FORGE.game.entities)
+ end
+
+ elseif stage == 7 then
+ -- ESC on unsaved work warns, S saves, ESC then leaves; the chooser lists the file.
+ key(SCANCODE.ESCAPE)
+ if FORGE.game == nil then
+ debugPrint("GAME FAIL one ESC closed unsaved work")
+ end
+ key(SCANCODE.S)
+ key(SCANCODE.ESCAPE)
+ if FORGE.game ~= nil then
+ debugPrint("GAME FAIL ESC did not close a saved description")
+ end
+ local listed = false
+
+ for _, entry in ipairs(forgeFiles()) do
+ if entry.path == opened then
+ listed = true
+ end
+ end
+ debugPrint("GAME closed; the chooser lists what was saved: " .. tostring(listed))
+
+ elseif stage == 8 then
+ -- Open it again from the chooser -- it is listed first -- and the rename is in the file.
+ for _ = 1, 20 do
+ key(SCANCODE.UP)
+ end
+ key(SCANCODE.RETURN)
+ if FORGE.game == nil then
+ debugPrint("GAME FAIL could not reopen the saved description")
+ else
+ local found = false
+
+ for _, entity in ipairs(FORGE.game.entities) do
+ if entity.id == "player" then
+ found = true
+ end
+ end
+ debugPrint("GAME reopened " .. FORGE.path .. "; the rename was saved: " .. tostring(found))
+ end
+
+ elseif stage == 9 then
+ -- B builds beside the runtime, which is what makes the result playable.
+ key(SCANCODE.B)
+ local runtime = io.open(singeGetDataPath() .. "Author.singe", "r")
+
+ if runtime == nil then
+ debugPrint("GAME FAIL build left no runtime beside the preview")
+ else
+ runtime:close()
+ debugPrint("GAME the build put the runtime beside the preview")
+ end
+ debugPrint("GAME RESULT done")
+ end
+end
+
+
+function onOverlayUpdate()
+ frames = frames + 1
+ if frames % 12 == 0 and stage < 10 then
+ stage = stage + 1
+ step()
+ end
+ forgeUpdate()
+ if frames == 6 or frames == 40 or frames == 60 or frames == 100 then
+ singeScreenshot()
+ end
+ if frames > 130 then
+ singeQuit()
+ end
+
+ return OVERLAY_UPDATED
+end