This commit is contained in:
Nick Beckley 2026-06-03 22:12:25 +01:00
parent 92ab864471
commit 2c48d7039a
26 changed files with 616 additions and 203 deletions

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxU227cNhD9FYGvFRXeRJF6c20HXaSOjeyiLfo2Ikc2a5laUJSTwPC/F9TauaJAXwRhSM45c+bMPJEID0h6cnY8Xj5izAupSQ5bSDChKdOUyYNgfct6ZZqOiVZI/TepSXiHn0lPOuUcdxapRI1UedbSQY6Oeo+WM+6ZaFXJCbcL6Z8IhMbjY3DYzMsfmJYwR9KTP0P088eCDaFx07z6Js0T7uKSIbpC5v3u/B39/ezmcH1zurXgUh43wZOeuNGC9+1InWGaKi4dNc4Jav2gZQuWeV9IQGhCzJgiTM3i77/i+zlHzK4XjZANo8KS55p4yFAoD7Dg4fOxsNgkuijxegtvv/0TecREelG/inlK98ZN4c3DMqxh8m+2L6nJMc1HTDngpsb1vvpKgrOGNUILxkhNrsDdhYjV7qK6nkqJ4AxT6CW1OHqqLAdqlZTUyxY6oceuEyOpyTtMEadvsl4Fl+ZlHnP1InL1Hc7+7PwywjBhwXgL04KkJrtlnyF6mOaIl5/QrfmU65DWcnw+xxziOq9LtYsZbxO8nL++P19TwpirG8h31W+w3JGeWDCOgwULoJ0bsWU4Ms9As86j9hq9Er7zyiqvlWVOKxi48tDyYSwRUpNfi4b71Tlclq9sNpNM08aheunT+RQw5u9kJD1hHIWXBukouKbKeKDQSku1lnpE7mwn+CvMAdItZtKT0QzDwJwbhLLcgbAg/cik16MVoutAjbLtlNC6ZR0433WCKy87dCBHyYwgNblJs19d/rHVcmvATZr/QZeLVKQnmrWtcTAMQituwBpUXCujLXZWjcCUdAPw1sAwcAYtc9YIB1xbUINvxy8qXcbbEPEiLMcJPn+DaxrdyF8cgOEjqB+uv03wgB/ndP/+ZOPm/eWB1OTDGstGqHbFIh9DpJ90GaardcrhcJcQPPqr2ePPPsIyLtU2odC6jolBUtWagapxRGqE41Sq0QzauRZ4eXExu3tMVXEYhFim6kuy6311ltxdyOjymgq/vzYe1/vqZoI8zunhu03yTWE/CtAIyVnpjMfH0zbaOGrvDXrNqVC8pcpaTo1tOzp0ZrCDNti6otj1mo9rrj6gDwld3ip+8eKGeX6H7v5nMV5d8GJRzorX9qcttkn7v5bYc00eEJY14cO2rfsnsosR04Z8sZ5GcRevwjSFBd0c/UJ627W8kZK/EPzPa6Y1jVTCPD8/P/8LAAD//wMA2D18ZxoGAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTwW7cOAyGX8Xg2XIkWZYt34JJFzvIthm0g+5ib7JIN0IceSDLaYtg3r2QZ4o2e9uLIBAi+f8fqVcI9pmgh9vT6d0LhbRACclvIcmlZlwzXh8l7xveq67SxnSqFv9CCf6evkMPrXJOOEOsJk1MIW/YUI+OIZIRXCCXjco17ZcF+lewvkJ68Y6qeflMcfFzgB7+9gHnr7m39ZWb5hWrOE+0D0uywWUxH/a7e/bX7eH4cLi8WmjJyZVH6MGNxiI2I3Md10yJ2rHOOckMDrpurOGIWYT1lQ+JYrBTteDTr/44p0DJ9bKSdcWZNHAuAW2yWfJgFzp+P2UVG6K7HC+38HbtX+GFIvSy/AnzUu7GTf7meRlWP+HN5Ux2ecouT3E+UUyeNiabzWmyyc+huHbaTZ5CghIePhWHyaZxjs9vQH26+N//D/vH3P3dN3JrItzNa0jQg1AcSnhv3aMPVOzvoAcuSGLdERul0Ex1aJltasO0rvVIwplWCihhN4fkwzqvS7EPib7EzQD08IedFrpIv43u0SdyaY3Z1T866zjEGVeXil/8Ba94VfOs5GFNpzUVHwl9JJco+zvG9VrvPylSyy1pt8ZIIRUHmx6LP+3yCD0Y2zlhjTXWaudGajiNHLnVvEXSqAmVxBaVUaiV4U4rOwiFthHDmCNQAuVxF9uGDW0nBFeOjdIZpnCUbGikYGpQTav1MNa2ecOxeJhynnUdV4Q1MzQiU0ZYZlRdM6wb20o9tq0cf0Nynb7gGTDSy+WvbAo0YkeoBZNKNEwZI1hnmpYNbTeYQXfUuKz5nmKg6TdQ772L8zKPqbguT/EG3cc15P9ebIv01Qf2bZvR3eyeKBZ5xtaHvN7XsZ7P5/MPAAAA//8DAK6M6YU2BAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSU227cNhCGX0XgtSjzJB50Z9gt6rqpF/EiLXpHcUa2EC21oKhNA2PfPaB2g8S9650w0D+Hb/7hG4n+gKQjt8fjLyeMeSE1yeMWEkxoyjRlci9Y14qOy8ZI4ZTR/5CajI/4lXTEqBB4cEglaqQKWEt7OQQKgI4zDky0quT0Lwvp3ogfG8DTGLCZl0+YlnGOpCN/jRHmL6W2H5swzSs0aZ7wIS7Zx1Ca+fPh7pH+cbvbP+0ufy24FHEzAukICwi8B00HGThVjvXUOQsUFPPGuNahMBfZGDOm6Kdmgc8/6sOcI+bQiUbIhlHhyLkm4LMvLfd+wf3XY+liQ3Rf4vUW3j67N3LCRDpRf4d5SXcTpvEmz8cJTzgdfVow3YT5cPARSE2OaT5iyiNuWLZJp8nncY7VtdjdNGLMpCZPz9Vu8nmY0+Edq+cLgof/QeCEqScdkayV2hhUUkrvdC8YgtFBa9EHz8zQC+QBWeuUcdwwLp1gTrbaWAOeuUBq8jLNvZ9+XzZ+TrDeAjNCC1TohNLaBYbS4tAaZOC17rUNDqQ1Qlmw0jvvuevRBKdDMcgHH17HiNXDfZmGowBpkQ6Ca6oseOpb6ajWUg/IgzOCk5rczTGPcZ3XpXqIGV/Sxo905Fc/LXghd5vC65gx5DUVqH/rUmyXZlhDrn44gLOGNZKxIlrzcc3VR4QxYchY8O7Tes33H4nQYhPdrSlhzNXO59fqN7+8FireBu5dGVWHMGDLcGDAvGYGUINGUAIMKKdAK8eCVr7nCnzL+6FESE2wGK7aPD4w5wUoQyXDlipoLbVCIRUtSik1NyrYdxyrp6nofLBMIUjqcACqHPfUKSkpyNYboQdjxPATkqv5OCuAAU+Xa9060AAWQXMqFG+pco5T61pDe2N712uLbSg9P2KKOP0E6sMY0rzMQ66u3q3eofu4xvLiVJuPv4yR/rvt6H4OnzFVZcd+jOXArms91+SAflkTHrYHq3sju3Jd1X57t4RthDK6Js/Zp7wer2HpGqWZOZ/P528AAAD//wMAJUGrCvUEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G36MJNvYgXbdAbxRnFRLTUgqI2DYz97wW1WyTuLTdhoJl555mX80aiOyLpyf3p9MsZY15ITXLYQoIJTZmmTB4E61vRc9mYjjHD9N+kJuEJv5GeGOU99xapRI1UAWvpIEdPAdByxoGJVpWa7vNC+jfiQgN4Dh6befkT0xLmSHryV4gwfy29XWj8NK/QpHnCXVyyi76I+WP38ER/v98fnvfXvxZcSnITgPSEeQQ+gKaj9JwqywZqbQcUFHPG2NaiMNe0EDOm6KZmgS/f+8OcI2bfi0bIhlFhyaUm4LIrkge34OHbqajYED2WeL2Ft8/+jZwxkV7U/8G8lrvzU7hb1mHCM04nlxZMd34+Hl0EUpNTmk+YcsANyxnTMC8hF6LeG9d1nEPnjGoNdLIV0oBkCB0zLViwTis9Ct+2IwydNWpUILgCjUoNZdAN3DS5HOZY3bQ/TAFjJjV5fqn2k8vjnI7v0L9cie5+AmiRTXoiWSu1MaiklM7qQTAEo73WYvCOmXEQyD2y1ipjuWFcWsGsbLXpDDhmPanJB+dfQ8Rq91i6cxQgO6Sj4JqqDhx1rbRUa6lH5N4awUlNHuaYQ1zndal2MePntM1LevKrmxa8Tnqf/GvI6POaCoRPuphxn2ZYfa6+G4CzhjWSsZK05tOaq48IIaHPWHAc0nqr978UocWW9LCmhDFXe5dfq9/c8kp6Yl3nubPOOqe9H7FlODJgTjMDqEEjKAEGlFWglWVeKzdwBa7lw1gipCZY/FZtFh8QmJNS0VEwQxVrgdrRSOql6vzoQIyGveNYPU8lz/mOKQRJLY5AleWOWiUlBdk6I/RojBh/QHIzC2cFMOD5+lg3BRqgQ9CcCsVbqqzltLOtoYPpBjvoDltfND9hijj9AOpD8Gle5jFXN69V79B9XGM5ONXmu68h0n+2HT3O/gumquzYhVje122tl5oc0S1rwuN2r/o3si+PqzpsZ0t0jVBG1+Qlu5TX0y0sbaM0M5fL5fIvAAAA//8DAKi9M8D0BAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUXW/jKBSG/4rFtXEBY2x81013tFGn02ianV3tHYbjFNWBCHBnqyr/fYSTqumM9mLvrGPe8/GcF16RU3tAPbo+HH5/BpciKlGyS4gRJjARmNRbRvqG9bSuOlqTtqH/oBLZW3hBPWq51lRLwDUIwNyQBg/1qLExICmhhrCG55xqF1H/ipStDDxbDZWP3yBE6x3q0V/WGf8911a20pOfTRX8BGsXk3I6N/NlvbrFn6832/vN6VSEmMWVNahHRIOhgxF4rDXFXJIBS9kZbDhRbSsbCaw9yaxLEJyaqmie3usbnxwk3bOK1RXBTKJjiYxKKrc8qAjbl0PuYkF0k+PlEl4++1f0DAH1rHyDeUp3pSd7tY/DbCdzNfndzrrdyrvR7uagUi5cokPwBwjJwgLnN+tUePnsd7ucD31SUwRUoi2EvXVqOv24gVHNU3rwc1jIRPP0X2dQj9ScPCrRQnKalrLFeZjVZMGlX7QXhe8fis2k0ujD/sOSHk7s1/8D/Z3Sj9ZBsb7JGgrM1B3gkVGBeWcUVk0tsRC1GIFq2TKKSrTyLlk3+zkWa5dgd6b2ob/roB9tAp3mkGf6W2SzbYI3s07F+4IpqUhVE5JFczrMqfgKxgbQCfIQ2zCf8/0kYYItok92gl/wrOYQwKVio9Jj8YeKj6hHUnWaKqmkUkLrERoCIzFECdIaEEaA4cy0hktuBJdEC64Gyo1q6DDmCCoRZJMVi69H08qRK4EJAYY5yA4rLmoMAxsbomhXM/IBbnE/ZZ3SHeFgaixhNJhLqrDkdY1N3aiWibFt2XjB6WwISjL1SxP+GcGcvfTlZO232Q08n+7x0qcwpgMjKGacNphLSXEnmxYPbTfIQXTQ6DzZyrvo30B+gzD4aFN+RO6ss3s1/XzkfTO3EBxMF9u5szr46MdUnG1ZfNjX19nlV6xYLPrdOvzvYowbr58gFNlYyrqLZR6Px+MPAAAA//8DAHv/g4QMBQAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRU0W7cKBT9FYtn4wDG2PgtmrTaUbabUTvqrvYNc68bFAdGgNNG0fz7Cs+s2jyt9gWha87l3HMOfiPePCMZye3p9OEFfU6kJtltJcGEokxR1h4FGzsxctkMsheKq79JTdw9vpKR9NJabjXSFhVSCayjUztbCoCaMw5MdLL0NN8SGd+IcQ3gi7PYhPQVY3LBk5H86TyE7+Vu4xq7hBWaGBbc+5SNt4XMH/vdPf399nB8OFxOJUwF3DggI2EWgU+g6NxaTqVmE9V6AAqSmb7XnUbRX2DOZ4zeLE2Cp5/3Q8gesx1FI9qGUaHJuSZgsimUJ5Pw+HoqLDaJ7kq93srbdnwjLxjJKOp/xby0u7GLu3lO0+oWuLms2aSntK2zsTnEV1KTUwwnjNnhps9tSvg8La9Hk54+Xs6UbfrwA+2aEXZh9ZmMhLcdqckm0LKY7IKvrhx3i0Oft485Op+c/a9eLanJw5fqsJg8h/j8zo8vF5n3/0PlT8Y+Oo/V/q5gOApoB6Sz4IrKAQw1XaupUq2akVvdC05qsgs+O7+GNVV7n/Fb3EYiI/loloQXfrfRPrqMNq+xzPmXKrk6xACrzdVPLzlrWNMyVkBrPq25+ozgItqMZYhjXK/9vr6HCCU20G6NEX2uDiY/Vr+Z9EhGos1gudFGG6OsnbFjODNgRrEeUIFCkAJ6kFqCkppZJc3EJZiOT3OpkJpgiU61pVXOOEvVWipkK6nkglPDUFGrJ637iTOtxDsdq4el4IwdmERoqcYZqNTcUC3blkLbmV6oue/F/Isk1zxwVgQGfLm8u42BAhgQFKdC8o5KrTkddNfTqR8mPakBO1s432P0uPwi1CdnY0hhztU1IdU76T6vvvw7qi0t352nPzaP7oJ9wlgVj43z5alcbT2fz+d/AAAA//8DAHLELVyCBAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUS2/bOBD+KwLPpkJSFCUK2IPhtFgjTWMk7u5ibxQ5jInIokFRToMi/31BWa6dooe9CaN5fY/hD9SrPaAGLQ+HT0fo44AWKLopxAgTmAhMii0jTckaynPGikKw8l+0QO4O3lCDKq411RJwAQIwN6TEbWE1NgYkJdQQVvLUUz0PqPmBlMsNHJ2G3A9/QRic71GD/na98a9ptnK57vxo8uA7WPdDVL1Oy3xdr+7wl+Vm+7A5ZQ0wpOLcGdQgosHQ1ghsC00xl6TFUtYGG05UVclSAqtOZa6PEHrV5YN5ucw3PvYQdcNyVuQEM4neF8ioqNLKrRpg+3ZIW0wU3ab4YgpPn80PdISAGrY4k3lqd6M7d7Mf2tF15iaq8AzRBrWHVx9e4Kg6tECH4A8QooOJm4en7LISJTnJmWCEoAW6V3rnesjWt9lDlwArXRMOpsASrMFcUoUlLwpsilJVTNiqYhYt0B2EHrqrrvdOBz94G7OZ8uzDnJXvo+tHPw7Zuo/wHFQ81X1W3QBogTZj27lh9wjKvG3945j+9WOXsKzGEKCP2UbFXfanGnaoQVLVmiqppFJCawslAUsMUYJUBoQRYDgzleGSG8El0YKrlnKjStraFEEL9DDGwxjP/H+/WmIb3H4P5rLBduJ406lofdivDfTRWZekOWdMhuq6CVU291x1Dvr4geTkKArMFDVgy6jAvDYKq7KQWIhCWKBaVoyeCLPuefzJE7HW8MKWdS1a2VICzFY1J5VtidG20NKULVSsIlUNtdSEtCWndSs1MUBLbhK64M2o469WKCaBHsc+XWe2TrBfXY+/C35hZBN89Np3STDXwdPbEGH/O9VWfn/wg4sJv52l/TbAve/T3/n+Z87m4qWPlxikO8im05M1qbRmLabGUMw1lbitoMVVVZd1KWxZ6ETUrdcvELJkMOX6SZOzpx6esmXQOxdBxzGkyf9MoE5yfj6fzIWP/Oun7coHWB4Oizn6xzGRdGp2NsCHd+XbkNJ3fohXkGdoT9DZ82JXdjJwPD1VLgWFMTUYQTHjtMRcSoprWVa4repWtqKGUl/smj2CcQF0nPptw5imzcz+zpfLEJ1VOg7peL54Pdlp9uec8uuSV7L9rD4Nv5JuttLciZKkxNPp4Zwc9L/ezff39/f/AAAA//8DAKqnhx8oBgAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTQW/cIBCF/4rF2TiAMTa+RZtWXaVtVk3UVr1hZtxFdWCFcZoo2v9e4d0qSW+9WWPe8OZ7wzPx5h5JTy4Ph3cP6NNMSpLcWhJMKMoUZfWdYH0jei6rrulq2ckfpCTuGp9IT1ppLbcaaY0KqQTW0KEeLQVAzRkHJhqZe5qfM+mfiXEV4IOzWIX5K8bZBU968s15CL/z3cZVdgoLVDFMuPVzMt5mM5+3m2v68XJ3d7M7nZpxzuLKAekJswh8AEXH2nIqNRuo1h1QkMy0rW40ivYkcz5h9GaqZvj1cj+E5DHZXlSirhgVmhxLAiaZbHkwM949HbKLFdFVrpdref3sn8kDRtKL8i/MU7sLO7kLG+7vjYeL0Xk370lJDjEcMCaHK451wmkyyQVfnC/ZTA59IiW5uS12k0ljiPdvGN2eRt/+x+SfjN07j8X2Kms4Cqg7pKPgisoODDVNralStRqRW90KTkqyCT45v4RlLrY+4c+4uiQ9eW+mGU/+LqPdu4Q2LTFb/65y1vjo0iZALjBSkl0MsNhUvODmrGJVzfLPmyUdllR8QXARbcI8011czu3/kQglVtFmiRF9KnYm7YsPZt6TnmjTWW600cYoa0dsGI4MmFGsBVSgEKSAFqSWoKRmVkkzcAmm4cOYK9l3TrdYF0qhaZtWCjrYQVNZjxnRwChvOrRdbaHVedIXrMXNlHXGdkwi1FTjCFRqbqiWdU2hbkwr1Ni2YnyF5Jw4Z5k34MPpaZwcAHQIilMheUOl1px2umnp0HaDHlSHjc2erzF6nF6B+uRsDHMYU3FemOINui+Lz8+7WJfnt/P0cY3sKthfGIscuXE+b/M55ePxePwDAAD//wMAoa2QhCUEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxU227jNhD9FYGvFRXeRIl6S50saqRJjLXRFn0bkaOYjUwZFJXsIsi/F5KTzWVRoC+CMCTnnDlzZp5IgAOShpwfj5cPGNJIcpL8EhJMaMo0ZXInWFOKhquiLquKCf43yYm/wu+kIZWylluDVKJGqhwraSs7S51Dwxl3TJRqzgl3I2meCPjC4YO3WAzjHxhHPwTSkD99cMPjjA2+sP0wuSIOPa7DmCDYmczNenVFfz/f7G43p1sjjvPjwjvSEGbR8dZp2knLqTKspcbUjjrFoKpMaVBUp2c+JIwB+mJ092/4bkgBk21EIWTBqDDkOScOEsyUWxhx9/04s1gkupjj+RJefpsn8oCRNCJ/FfOU7sz2/uwwtpPv3dnyJTk5xuGIMXlc1LjdZm8kOCtYIbRgjOTkGuzeB8zWF9ltP5cItmYKnaQGO0eV4UCNkpI6WUIldFdVoiM5ucIYsH+X9drbOIxDl7IXkbMPONvz1WWAtscZ4wv0I5KcrMdtguCgHwJefkM7pVOuXZzm49UQkg/TMI3ZOiS8i/By/vp+NcWIIWUbSPvsNxj3pCEGasvBgAHQ1nZYMuyYY6BZ5VA7jU4JVzlllNPKMKsVtFw5KHnbzRGSk19nDbeTtTiOb2wWk/T9wiF76dOq9xjSBxlnl3AUTtZIO8E1VbUDCqU0VGupO+TWVIK/wuwg3mEiDenqtm2Zta1QhlsQBqTrmHS6M0JUFahOlpUSWpesAuuqSnDlZIUWZCdZLUhONnFwk02fWy2XBmzi8A/aNEtFGqJZWdYW2lZoxWswNSquVa0NVkZ1wJS0LfCyhrblDEpmTS0scG1Ata7sfqh0Ge58wAs/Hnv4/g63LnQhf7EANe9Afbr+JcIBH4d4f3OycXFzuSM5+TqFeSNk69kijz7Qb3qe6OupT363jwgO3fXg8Gcf4Twu2WlCEZC5rqNStZyq1nbUlK2ilTOVlqhQLmwuBnuPMZsdBj7MU/Uj2e02O4927xPaNMWZ318Lj9tttukhdUM8fNgk7wr7LEAhJGdzZxw+nLbRwlE7V6PTnArFS6qM4bQ2ZUXbqm5Nq2ss7czxdkrHKWVf0fmINi0Vv3hxwVzt0d7/LMarC14sytnste1piy3S/q8l9pyTA8I4RTws27p5IusQMC7IF9NpFNfh2ve9H9EOwY2k4axkBTOleGH4n/c454ViSj0/Pz//CwAA//8DADl0x2IbBgAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTTW/cNhD9K8KcRZmkKErUzVin6MJNvEgWadEbxRnFhGVqQVFOAmP/e0DtBol764UgBnwz72P4CsE+E/Rwezq9e6GQFigh+a0kudSMa8bro+R9I3uhqk6ZlvPmXyjB39N36KFVzglniNWkiSnkDRvq0TFEMoIL5LJRuaf9skD/CtZXSC/eUTUvnykufg7Qw98+4Pw1z7a+ctO8YhXnifZhSTa4TObDfnfP/ro9HB8Ol1cLLRlceYQeuCMUA2o21k4wZfjAjOmQoeK2bU1jSLYXmA+JYrBTteDTr/k4p0DJ9bKSdcWZNHAuAW2ymfJgFzp+P2UWm0V3uV5u5e3av8ILRehl+dPMS7sbN/mb52VY/YQ3lzPZ5SmrPMX5RDF52jzZZE6TTX4OxXXSbvIUEpTw8Kk4TDaNc3x+Y9Sni/79/5B/zNPffSO3JsLdvIYEPYi6gxLeW/foAxX7u9xOkMS6IzZKoZnq0DLb1IZpXeuRhDOtFFDCbg7Jh3Vel2IfEn2JmwDo4Q87LXShfhvdo0/k0hqzqn903oVDnHF1qfjlv+AVr2rOM2hNpzUVHwl9JJco6zvG9drvPxCp5QbarTFSSMXBpsfiT7s8Qg/Gdk5YY4212rmRGk4jR241b5E0akIlsUVlFGpluNPKDkKhbcQw5gqUQDnuYtswzQV2TS2YJeyYItMwI7uRdQpHQW1jsHFvfCwepoyzruOKsGaGRmTKCMuMqmuGdWNbqce2leNvllzTFzwbjPRy+SsXBogdoRZMKtEwZYxgnWlaNrTdYAbdUeMy53uKgabfjHrvXZyXeUzFdXmKN9Z9XEP+78W2SF99YN+2jO5m90SxyBlbH/J6X2M9n8/nHwAAAP//AwCAbbUbNgQAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUTW/cNhT8KwLPoswvUaJuxjpBF27qRbxIi96eyKeYsEwtSMqJYfi/F9Ru4fhU9EIQT5zhvHlDvZIAT0gGcn06fXrGkBOpSfZbSTChKdOUyaPgA2sHxRvRcqa0+JvUxN/iCxlIp6zl1iCVqJEqx1o6yslS59Bwxh0TrSqc8D2R4ZWAbxw+e4vNkr5hTH4JZCB/+uCWH+Vu8I2dl9U1cZlxH1KGYIuYP/a7W/r79eF4dzifSpgKuPGODISJzkwOJOW8R6q4QDoaI6kb+87YiWsm8QzzIWMMMDfJPb7f75YcMNtBNEI2jApD3mriIEORPELC48upqNgsuin1eitv2+GVPGMkg6j/NfNMd2Vnf/WUxtXP7uq8ZkiPaVsnsHmJL6Qmp7icMGaPmz/XKeHTOL8cIT1+Pp8p2/TpJ9o1o9sta8hkIFx2pCabQfMM2S+humjczR5D3j7m6EPy9r+4JKnJ3X11mCFPS3z6MI/7s837/+HyF7APPmC1vykYjsLJHukkuKaqd0ChlYZqLfWE3JpOcFKT3RKyD+uypmofMn6PW0tkIJ9hToX07r66jvbBZ7R5jaXPv3TJ1SEubrW5ep8lZw1rJGMFtObTmquv6HxEm7E0cYzrhe/bR4jQYgPt1hgx5OoA+aH6DdIDGYiB3nIwYAC0tRO2DCfmGGjWOdROo1PCdU4Z5bQyzGoFI1cOWj5OpUJqgiU61ZZWhXxkMGkq+xaogqml0AJQ1Jx1yB32ogTs3cfqbi44sD1T6CQ1ODmqDAdqlJTUyRY6oaeuE9MvllzywFkx2OHz+d1tCrRzPTrNqVC8pcoYTnvTdnTs+tGMusfWFs23GAPOvxj1xdu4pGXK1SUh1Qfrvq6h/DuqLS0/fKA/txndLPYRY1VmDD6Up3IZ69vb29s/AAAA//8DAHHCgySCBAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUW2/cNhCF/4rAZ1HmTbzozbBb1HDTLGKjLfo24oxiIVppQVGbBob/e0HtBqn71jdiwDM8/OaQr2yGI7GO3Z5OP51pziurWR73khLKcmG50M9KdsJ3om28Fa2R7i9Ws/GRvrGOOROjjIG4JkvcoGh5r4fIESlIIVGo1pSe8Hll3SuDsUE6j5GaZf2d0jouM+vYH+OMy9dyNoxNnJYNm7RM9DCvGeZYzPz2cPfIf709PH88XHattBZxMyLrGMlotGg1V0SGmx48D1FabgbyPrSWwOBFNs6Z0gxTs+KXH+fjkmfKsVON0o3gKrC3miFkKJZ7WOn526m42BHdl3q9l/dl98rOlFin6u8wL+1u4jTerFs/0ZmmE6SV0k1cjkeYi5dTWk6U8kg7ljOlflnHXIjG6MB7KdGDM61Dr1ulHWpB6IVrMWAAa+ygYtsO2PvgzGBQSYOWjOkdq9kObpogj8tcXb3fTSPNmdXs41N1mCAPSzq+Q/90IfrwP4AW26xjZad1jozWGoLtlSB0Nlqr+gjCDb0iGUm0wbggnZA6KBF0a513CCJEVrMPEF/GmaqHe9YxIUmh9sQHVc70CBxaHbi12g4kY3BKsprdLXMe523Z1uphzvQ57fdlHfsZppUuN71N8WXMFPOWCoQ/bQnjIS24xVz9CIAUjWi0EEW05dOWq0+EY6KYqeB4Ttu1338kyqpddLelRHOuDpBfql9gfWEdC+CjhAABwMY4UCtoECjACodk0RIahQ5NMGhNENEa6KVBaGU/lAqrGZW8VXvEfQShvYw8Bim4MWB5j0JzUL0IAw7Kq+Edx+rjVHQQvTCEmgcakJsggQejNUfdglN2cG7XfUdyDYsUBTDS+fJYdwcW0RNayZWRLTchSO5D63jvfB9666mNxfMjpZmmf4H6MMa0rMuQq2vWqnfoPm1z+XCqPXdfx5n/vc/ofolfKFVlxjDO5X1dx/pWsyPBuiU67v9V98oO5XFVz/u3pXxjhbE1e8qQ8na6lrVvtPX+7e3t7R8AAAD//wMAMdQZgPQEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUwW7jNhCGX0XgWVQoiqJE3VKnixrZbIyNuy16G5Ejh4hMGhSVbRD43ReUHcTZRQ+9CaP5OTPf/OQrcbBH0pHrw+H3Z3RxIjmJdglxxiVlkrJqy8uOtR2ri1ZVbSP4PyQn9hZfSEcaoXWpFdIKJVJhWE37atDUGFQlKw3jtUhnwm4i3SsBWxh8thoLP33DMFnvSEf+ss7476k22EKPfjZF8COu3RTB6dTMl/Xqln6+3mzvN6esCackLqwhHcFSi4rVFeWIgooeWqp0KakYsG1VLRGEOcmsixgcjMVknt7rGx8dRt3xglcFo1yRY04MREgt9zDh9uWQulgQ3aR4voSXz+6VPGMgHc/fYJ6Ou9KjvdpP/WxHczX63c663cq7we7mADEVzskh+AOGaHGB85t1EF4++90unUc+wTghyckWw946GE8/bnCAeYwPfg4Lmck8/VcO6QjM0ZOcLCTHcSmbnYdZjRZd/EV7Ufj+IduMEAcf9h+W9HBiv/4f6O9AP1qH2fqGdISVyE3VIh14ymwNUKgrRaWs5IClVg0vSU5W3kXrZj9P2dpF3J2pfejvOuhHG1HHOaSZ/pbJbJvgzaxj9r7gkhWsqBhLojke5ph9RWMD6ohpiG2Yz+f9JOGSL6JPdsRf8KzmENDFbAPxMfsDpkfSEQWtLkGBApBaD1gzHJhhIFljUBqJRnDTGKGEkUIxLQX0pTBQl/2QIiQnmEyWLb4uoeW8NjVVyNPl4oz2gxK0FwDMVFpAKT7Aze7HpAPdMoGmogoHQ4UqgSpRVdRUNTRcDk3DhwtOZ0OULFG/NOGfE5qzl76crP02u8Hn0z1e+pTGtGhkSbkoayqUKmmr6ob2TdurXrZY6zTZyrvJv4H8hqH3k43pEbmzzu5h/DnlfTO3GByOF9u5szr4yQ8xO9sy+7Cvr7NLr1i2WPS7dfTfxRg3Xj9hyJKxwLqLZR6Px+MPAAAA//8DAJxlZlgMBQAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS0NSN8NuUddNvYgXadEbRY5sIVpqQVGbBsb+94DaDRL31hsx4Hw98868kegOSDpyezz+csKYF1KTPG4mwQRQBpTJveAdMx1rG8OVYQr+ITUZH/Er6YhW3nNvkUoEpCqwlvZy8DQEtJzxwESrSkz3spDujbixCXgaPTbz8gnTMs6RdOSvMYb5S8ntxsZP8xqaNE/4EJfsoi/F/Plw90j/uN3tn3aXXwsuxbkZA+kIcq8kayUViIqq3hlqPQeqBjTGtoBOhYvbGDOm6KZmCZ9/5A9zjph9JxohG0aFJeeaBJddKbl3C+6/HksVG6L7Yq838/bs3sgJE+lE/R3mJdyNn8abPB8nPOF0dGnBdOPnw8HFUssxzUdMecQNy9bpNLk8zrG6JrubRoyZ1OTpudpNLg9zOrxj9XxB8PA/CJww9aQj5SdojUpK6Sz0gmHQ4AFE7x3TQy+Qe2StVdpyzbi0glnZgjY6OGY9qcnLNPdu+n3Z+FnBehOYFiBQoRUKwHqG0uDQamTBAfRgvA3SaKFMMNJZ57jtUXsLvgjkg/OvY8Tq4Z50hHEUQRqkgyg9mOCoa6WlABIG5N5qwUlN7uaYx7jO61I9xIwvaeNHOvKrmxa8kLtN/nXM6POaCtS/oSTbpTmsPlc/FMBZwxrJWHFa83HN1UcMY0KfseDdp/Ua7z8uAsTmdLemhDFXO5dfq9/c8lqoOOO5s6VV8H7AluHAAnPAdEAIgEGJoIOyKoCyzINyPVfBtbwfioXUBIvgqk3jxljuB9ZSQFPmKgZqADQV2hhlWyOgh3ccq6ep+DlvmMIgqcUhUGW5o1ZJSYNsnRYwaC2Gn5BcxcdZARzwdNnWrQIIwWAAToXiLVXWcmpsq2mvTW97MNj6UvMjpojTT6A+jD7Nyzzk6qrd6h26j2ssF6fadPxljPTfbUb3s/+MqSozdmMsC3Yd67kmB3TLmvCwHazujezKdlX77W4J0wBTUJPn7FJej1ezNI0EY87n8/kbAAAA//8DAKMP1wj1BAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUXW/lJhD9KxavNV7AGIPf0iSrXm2zifZetVXfBhgnNA6+wji7qyj/vcI32U9V6otlDcycw5kz80QiPCAZyNnxePmIMS+kJjlsIcGEokxR1h4EH5gemGoM00p3+m9Sk/AOP5OB9NI57gzSFhVS6VlHbTs66j0azrhnopOlJtwuZHgiEBqPj8FhMy9/YFrCHMlA/gzRzx8LNoTGTfPqmzRPuItLhugKmfe783f097Obw/XN6daCS0lugicDQe5ky7qWCkRJpQVNjeOKyhG1Np1CkP6UFmLGFGFqFn//Fd/POWJ2g2hE2zAqDHmuiYcMhbKFBQ+fj4XFJtFFiddbePsdnsgjJjKI+lXMU7k3bgpvHha7hsm/2b6kJsc0HzHlgJsa1/vqKwnOGtYIJRgjNbkCdxciVruL6noqTwSnmUTfUoOjp9JwoEa2LfVtB71QY9+LkdTkHaaI0zdVr4JL8zKPuXoRufoOZ392fhnBTlgw3sK0IKnJbtlniB6mOeLlJ3RrPtU6pLUcn88xh7jO61LtYsbbBC/nr/nna0oYc3UD+a76DZY7MhAD2nEwYACUcyN2DEfmGSjWe1ReoZfC914a6ZU0zCkJlksPHbdjiZCa/Fo03K/O4bJ8ZbOZZJo2DtVLn86ngDF/JyMZCOMofKuRjqJ4Q3ug0LWGKtWqEbkzveCvMAdIt5jJQEZtrWXOWSENdyAMtH5krVejEaLvQY5t10uhVMd6cL7vBZe+7dFBO7ZMC1KTmzT71eUfW91uDbhJ8z/ocpGKDESxrtMOrBVKcg1Go+RKamWwN3IEJltngXcarOUMOuaMFg64MiCt78YvKl3G2xDxIizHCT5/g6sb1bS/OADNR5A/XH+b4AE/zun+/cnGzfvLA6nJhzWWjVDtikU+hkg/qTLRV+uUw+EuIXj0V7PHn32EZVyqbUKtk8qjs9TwnlGplaZ69Ehtaz03thfeFKkuZnePqSoOgxDLVH0pdr2vzpK7CxldXlPh99fG43pf3UyQxzk9fLdJvnnYjwI0ouWswHl8PG2jjaPyXqNXnArJOyqN4VSbrqe219ZYpbFzRbHrNR/XXH1AHxK6vL34xYsb5vkduvufxXh1wYtFOSte25+22Cbt/1pizzV5QFjWhA/bth6eyC5GTBvyxXoaxV28CtMUFnRz9AsZOOOmMYzxF4b/fU+3jRCSPz8/P/8LAAD//wMANFneEhsGAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUTW/bOBD9KwLPpkJSFCUK2IPhtFgjTWMk7u5ibxQ5jInIokFRToMi/31BWa6dooe9CaP5eO/NG/5AvdoDatDycPh0hD4OaIGim0KMMIGJwKTYMtqQuiEiZ7Iuy0r8ixbI3cEbalDFtaZaAi5AAOaGlLgtrMbGgKSEGsJKnnqq5wE1P5ByuYGj05D74S8Ig/M9atDfrjf+Nc1WLtedH00efAfrfoiq1wnM1/XqDn9ZbrYPm1PWAEMqzp1BDQKqeUHKAjMAjnmraiw1FZhbqGtZClDcnMpcHyH0qssH83KZb3zsIeqG5azICWYSvS+QUVElyK0aYPt2SCgmiW5TfDGFp8/mBzpCQA1bnMU8tbvRnbvZD+3oOnMTVXiGaIPaw6sPL3BUHVqgQ/AHCNHBpM3DU3aBRElOciYYIWiB7pXeuR6y9W320CXCSteEgymwBGswl1RhyYsCm6JUFRO2qphFC3QHoYfuquu908EP3sZsljz7MGfl++j60Y9Dtu4jPAcVT3WfVTcAWqDN2HZu2D2CMm9b/zimf/3YJS6rMQToY7ZRcZf9qYYdapBUtaZKKqmU0NpCScASQ5QglQFhBBjOTGW45EZwSbTgqqXcqJK2NkXQAj2M8TDGs/7fr0Bsg9vvwVwQbCeNN52K1of92kAfnXVpNeeMyVBdN7HK5p6rzkEfP4iMGkQoMFPUgC1LPqqNwqosJBaiEBaolhWjJ8Gsex5/6kSsNbywZV2LVraUALNVzUllW2K0LbQ0ZQsVq0hVQy01IW3Jad1KTQzQcjLpJngz6virFYppQY9jn64zWyfar67H3wW/KLIJPnrtu7Qw18HT2xBh/7utrfz+4AcXE387r/bbAPe+T3/n+581m4uXPl5ikO4gm06vJXXLdalxS+syORFwqwuBCSeU1azlXCXgt16/QMiSwZTrp52cPfXwlC2D3rkIOo4hTf5nInVa5+fzyVz0yL9+2q58gOXhsJijfxyTSKdmZwN8eFe+DSl954d4RXmm9gSdPQO7spOB4+mpcikojKnBCIoZp4mnpLiWZYXbqm5lK2oo9cWu2SMYF0DHqd82jGnarOzvfLkM0Vml45CO54vXk51mf84pv4K8WtvP6tPwq9XNVpo7UZIs+3R6OCcH/a938/39/f0/AAAA//8DAKL+ZjEoBgAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTwW7cNhD9FWHOokxSFCXqZqxTdOEmXiSLtOiN4oxiwjK1oCgngbH/HlC7gZPeeiGIAd/Me28eXyHYZ4Iebk+ndy8U0gIlJL+VJJeacc14fZSi513PdWW4lnXT/Asl+Hv6Dj20yjnhDLGaNDGFvGFDPTqGSEZwgVw2Kve0XxboX8H6CunFO6rm5TPFxc8BevjbB5y/5tnWV26aV6ziPNE+LMkGl8l82O/u2V+3h+PD4fJqoSWDK4/QAwmnat7UTBIppgbbMeOEZmqkrjONJqvwAvMhUQx2qhZ8epuPcwqUXC8rWVecSQPnEtAmmykPdqHj91NmsVl0l+vlVt6u/Su8UIRelj/NvLS7cZO/eV6G1U94czmTXZ6yylOcTxSTp82TTeY02eTnUFwn7SZPIUEJD5+Kw2TTOMfn34z6dNG//x/yj3n6u2/k1kS4m9eQoAdRd1DCe+sefaBifwc9cEES647YKHOTDi2zTW2Y1rUeSTjTSgEl7OaQfFjndSn2IdGXuAmAHv6w00IX6rfRPfpELq0xq/pH5ywc4oyrS8Wb/4JXvKo5z6A1ndZUfCT0kVyirO8Y12u//0Cklhtot8ZIIRUHmx6LP+3yCD0Y2zlhjTXWaudGajiNHLnVvEXSqAmVxBaVUaiV4U4rOwiFthHDmCtQAuV1F1vCNFeOlOhYo3Bkamw7ZpREZtUw2m5ArXQOxZuPxcOUcdZ1XBHWzNCITBlhmVF1zbBubCv12LZy/MWS6/YFzwYjvVz+yoUBYkeoBZNKNEwZI1hnmpYNbTeYQXfUuMz5nmKg6Rej3nsX52UeU3ENT/GbdR/XkP97sQXpqw/s27aju9k9USzyjq0POd7XtZ7P5/MPAAAA//8DAEVhgyo2BAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTwW7UMBD9lcjnOLUd24lzq7YgVgW6ohUgbrP2mLVInZXjFFC1/46cXQTlxs169pt58974mUR4RDKQ6+Px1RPGPJOa5LBCgglNmaasfRB8YP3AdGNYr4xSX0hNwi3+JAPppLXcGqQtaqTSMUX3rbfUOTSccceEkqUmfJ3J8EwgNA6fgsVmmj9imsMUyUA+heim76U3hMaO0+KaNI24jXOGaIuY99vNLX17vXu4251fzTgXchMcGQhyK1umWioQJZV76KmxXFPpse+N0gjSnWkhZkwRxmZ23/70d1OOmO0gGtE2jApDTjVxkKFI3sOMDz+PRcVq0U3B6xVej8MzecJEBlH/NvNc7sqO4cpOj48Q3ZUPMcwHUpNjmo6YcsDVjnXCcYQcplhdmmzGgDGTmtzdV7sRsp/S4wuP7s+jb/9j8ndgDyFitb0hA2EchWt7pF6Ul70DCqo1VOtWe+TWdIKTmmymmENcpmWutjHj17SqJAN5DeOMZ33XyR5CRpuXVKR/1iVr/BHyZnIFYKQmuzS5xebqj92cNaxpWbm8W/JxydUHdCGhzVhmekjLpfw/FKHFStosKWHM1Q7yoXoD84EMxEBvORgwANpaj4qhZ46BZp1D7TQ6KVznpJFOS8OslrDn0oHie1+QorukW60LBd5DpxRSKxSjElugYHtLPbK+5b1R3r20tbobV57tmUTXUoPeUWk4UCPblrpWQSe07zrh/7LkkjhnxW+HT+evsSrQzvXoNKdCckWlMZz2RnV03/V7s9c9Kls032KKOP5l1Ltg0zRPPleXhaleWPdhieV7V+vyfA+R/lgju5nsN0xViRxCLNt8Sfl0Op1+AQAA//8DAKrhPt4lBAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUTW/cNhD9KwLPoswvUaJuxjpBF27qRbJIi94ozigmLFMLknJiGPvfC2q3SHwqeiGI4bzhmzePfCPBPiMZyO3p9OEFQ06kJtlvIcGEpkxTJo+CD6wfmG560zPJ5d+kJv4eX8lAOuUcdwapRI1UAWvpKCdHAdBwxoGJVpWa9lsiwxuxvgF88Q6bJX3FmPwSyED+9AGW7+Vu6xs3Lys0cZlxH1K2wRUyf+x39/T328Px4XDJSpgKuPFABoLcKclaSQWiomq0PTWOa6om7HvTarQKLjAfMsZg5ybB08/7YckBsxtEI2TDqDDkXBOw2RbKo014fD0VFptEdyVeb+FtO7yRF4xkEPW/Yl7K3bjZ3zyncfUz3FzWbNNT2tbJurzEV1KTU1xOGLPHTZ/blPB5nF+PNj19vOSUbfrwA92aEXbLGjIZCJctqckm0Dzb7JdQXTnuZo8hb4c5+pC8+69aktTk4Ut1mG2elvj8bh5fLjLv/4fKn6x79AGr/R0ZCOMoQPZIJ1Eye7DUttJQraWekDvTCU5qsltC9mFd1lTtQ8ZvcWuJDOSjnRNe+N1G9+gzurzG0udfuvjqEBdYXa5+zpKzhjWSsQJa82nN1WcEH9FlLE0c43qt9/U9RGixgXZrjBhydbD5sfrNpkcyEGN7x62xxlrt3IQtw4kBs5p1gBo0ghLQgTIKtDLMaWVHrsC2fJxKhNQEi3Wqza3cCeAcGVUcLFUTAzpaoWinuHE9cM2Zfqdj9TAXnHU9UwiSGpyAKsMtNUpKCrK1ndBT14npF0mufuCsCAz4cnl3GwMN0CNoToXiLVXGcNqbtqNj149m1D22rnC+xxhw/kWoT97FJS1Trq4Oqd5J93kN5e+oNrd894H+2GZ0t7gnjFWZsfWhPJXrWM/n8/kfAAAA//8DAANjiOSCBAAA

Binary file not shown.

View File

@ -1956,7 +1956,8 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
AttributeTypes = (await result.ReadAsync<CharacterAttributeType>()).ToList(),
KnowledgeStates = (await result.ReadAsync<KnowledgeState>()).ToList(),
RelationshipTypes = (await result.ReadAsync<RelationshipType>()).ToList(),
RelationshipStates = (await result.ReadAsync<RelationshipState>()).ToList()
RelationshipStates = (await result.ReadAsync<RelationshipState>()).ToList(),
RelationshipCategories = (await result.ReadAsync<RelationshipCategory>()).ToList()
};
}

View File

@ -626,6 +626,17 @@ public sealed class RelationshipType
public bool IsPermanentDefault { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; }
public int RelationshipCategoryID { get; set; }
public string RelationshipCategoryName { get; set; } = "Other";
public int CategorySortOrder { get; set; }
}
public sealed class RelationshipCategory
{
public int RelationshipCategoryID { get; set; }
public string CategoryName { get; set; } = string.Empty;
public int SortOrder { get; set; }
public bool IsActive { get; set; }
}
public sealed class RelationshipState
@ -646,6 +657,10 @@ public sealed class CharacterRelationship
public string CharacterBName { get; set; } = string.Empty;
public int RelationshipTypeID { get; set; }
public string RelationshipTypeName { get; set; } = string.Empty;
public int RelationshipCategoryID { get; set; }
public string RelationshipCategoryName { get; set; } = "Other";
public int CategorySortOrder { get; set; }
public int RelationshipTypeSortOrder { get; set; }
public bool IsPermanent { get; set; }
public int? StartSceneID { get; set; }
public int? EndSceneID { get; set; }
@ -674,6 +689,10 @@ public sealed class RelationshipEvent
public int CharacterBID { get; set; }
public string CharacterBName { get; set; } = string.Empty;
public string RelationshipTypeName { get; set; } = string.Empty;
public int RelationshipCategoryID { get; set; }
public string RelationshipCategoryName { get; set; } = "Other";
public int CategorySortOrder { get; set; }
public int RelationshipTypeSortOrder { get; set; }
public bool IsReciprocal { get; set; }
public int SceneID { get; set; }
public decimal SceneNumber { get; set; }
@ -697,6 +716,7 @@ public sealed class CharacterLookupData
public IReadOnlyList<KnowledgeState> KnowledgeStates { get; init; } = [];
public IReadOnlyList<RelationshipType> RelationshipTypes { get; init; } = [];
public IReadOnlyList<RelationshipState> RelationshipStates { get; init; } = [];
public IReadOnlyList<RelationshipCategory> RelationshipCategories { get; init; } = [];
}
public sealed class LocationType
@ -1130,6 +1150,10 @@ public sealed class StoryBibleRelationshipSummary
public int CharacterBID { get; set; }
public string CharacterBName { get; set; } = string.Empty;
public string RelationshipTypeName { get; set; } = string.Empty;
public int RelationshipCategoryID { get; set; }
public string RelationshipCategoryName { get; set; } = "Other";
public int CategorySortOrder { get; set; }
public int RelationshipTypeSortOrder { get; set; }
public string? InitialRelationshipName { get; set; }
public string? InitialStateName { get; set; }
public int? InitialIntensity { get; set; }

View File

@ -678,7 +678,7 @@ public sealed class SceneService(
model.CharacterAttributeTypeOptions = ChapterService.ToSelectList(characterLookups.AttributeTypes, x => x.CharacterAttributeTypeID, x => x.AttributeName);
model.KnowledgeStateOptions = ChapterService.ToSelectList(characterLookups.KnowledgeStates, x => x.KnowledgeStateID, x => x.StateName);
model.RelationshipOptions = ToOptionalSelectList(projectRelationships, x => x.CharacterRelationshipID, x => $"{x.CharacterAName} -> {x.CharacterBName} ({x.RelationshipTypeName})");
model.RelationshipTypeOptions = ToOptionalSelectList(characterLookups.RelationshipTypes, x => x.RelationshipTypeID, x => x.TypeName);
model.RelationshipTypeOptions = ToRelationshipTypeSelectList(characterLookups.RelationshipTypes);
model.RelationshipStateOptions = ChapterService.ToSelectList(characterLookups.RelationshipStates, x => x.RelationshipStateID, x => x.StateName);
model.LocationOptions = ToOptionalSelectList(projectLocations, x => x.LocationID, x => new string(' ', x.Depth * 2) + x.LocationName);
model.SceneOptions = ToOptionalSelectList(allSceneOptions.Where(x => x.SceneID != model.SceneID), x => x.SceneID, x => $"{x.BookTitle} / Ch {x.ChapterNumber} / Scene {x.SceneNumber}: {x.SceneTitle}");
@ -816,6 +816,23 @@ public sealed class SceneService(
private static IReadOnlyList<SelectListItem> ToOptionalSelectList<T>(IEnumerable<T> rows, Func<T, int> value, Func<T, string> text)
=> rows.Select(x => new SelectListItem(text(x), value(x).ToString())).ToList();
private static IReadOnlyList<SelectListItem> ToRelationshipTypeSelectList(IEnumerable<RelationshipType> relationshipTypes)
{
var groups = relationshipTypes
.GroupBy(x => string.IsNullOrWhiteSpace(x.RelationshipCategoryName) ? "Other" : x.RelationshipCategoryName)
.ToDictionary(x => x.Key, x => new SelectListGroup { Name = x.Key });
return relationshipTypes
.OrderBy(x => x.CategorySortOrder == 0 ? 900 : x.CategorySortOrder)
.ThenBy(x => x.SortOrder)
.ThenBy(x => x.TypeName)
.Select(x => new SelectListItem(x.TypeName, x.RelationshipTypeID.ToString())
{
Group = groups[string.IsNullOrWhiteSpace(x.RelationshipCategoryName) ? "Other" : x.RelationshipCategoryName]
})
.ToList();
}
public async Task<SceneMovePreviewViewModel?> GetMovePreviewAsync(SceneMoveRequestViewModel request)
{
var preview = await scenes.GetMovePreviewAsync(request.SceneID, request.TargetChapterID, request.Position);
@ -2446,7 +2463,7 @@ public sealed class ExportService(IExportRepository exports) : IExportService
if (r.InitialIntensity.HasValue) initial += $" (intensity {r.InitialIntensity})";
var current = r.CurrentStateName ?? "No current state";
if (r.CurrentIntensity.HasValue) current += $" (intensity {r.CurrentIntensity})";
builder.AppendLine($"- {r.CharacterAName} / {r.CharacterBName}: Initial: {initial}; Current: {current}; Events: {r.EventHistory ?? "No event history"}");
builder.AppendLine($"- {r.CharacterAName} / {r.CharacterBName} [{r.RelationshipCategoryName}]: Initial: {initial}; Current: {current}; Events: {r.EventHistory ?? "No event history"}");
}
builder.AppendLine();
}
@ -3211,7 +3228,9 @@ public sealed class CharacterService(IProjectRepository projects, IBookRepositor
Events = events
};
})
.OrderBy(x => x.Relationship.CharacterAName)
.OrderBy(x => x.Relationship.CategorySortOrder == 0 ? 900 : x.Relationship.CategorySortOrder)
.ThenBy(x => x.Relationship.RelationshipTypeSortOrder)
.ThenBy(x => x.Relationship.CharacterAName)
.ThenBy(x => x.Relationship.CharacterBName)
.ThenBy(x => x.Relationship.RelationshipTypeName)
.ToList(),
@ -3226,7 +3245,7 @@ public sealed class CharacterService(IProjectRepository projects, IBookRepositor
IsReciprocal = true
},
CharacterOptions = projectCharacters.Select(x => new SelectListItem(x.CharacterName, x.CharacterID.ToString())).ToList(),
RelationshipTypeOptions = ChapterService.ToSelectList(relationshipLookups.RelationshipTypes, x => x.RelationshipTypeID, x => x.TypeName),
RelationshipTypeOptions = ToRelationshipTypeSelectList(relationshipLookups.RelationshipTypes),
RelationshipStateOptions = relationshipLookups.RelationshipStates.Select(x => new SelectListItem(x.StateName, x.RelationshipStateID.ToString())).ToList(),
BookOptions = projectBooks.Select(x => new SelectListItem($"Book {x.BookNumber}: {x.BookTitle}", x.BookID.ToString())).ToList()
};
@ -3408,6 +3427,23 @@ public sealed class CharacterService(IProjectRepository projects, IBookRepositor
: "Unknown";
}
private static IReadOnlyList<SelectListItem> ToRelationshipTypeSelectList(IEnumerable<RelationshipType> relationshipTypes)
{
var groups = relationshipTypes
.GroupBy(x => string.IsNullOrWhiteSpace(x.RelationshipCategoryName) ? "Other" : x.RelationshipCategoryName)
.ToDictionary(x => x.Key, x => new SelectListGroup { Name = x.Key });
return relationshipTypes
.OrderBy(x => x.CategorySortOrder == 0 ? 900 : x.CategorySortOrder)
.ThenBy(x => x.SortOrder)
.ThenBy(x => x.TypeName)
.Select(x => new SelectListItem(x.TypeName, x.RelationshipTypeID.ToString())
{
Group = groups[string.IsNullOrWhiteSpace(x.RelationshipCategoryName) ? "Other" : x.RelationshipCategoryName]
})
.ToList();
}
private async Task<IReadOnlyList<SceneCharacter>> GetAppearancesByCharacterAsync(int characterId)
{
var timeline = await characters.GetTimelineAsync((await characters.GetCharacterAsync(characterId))?.ProjectID ?? 0, null);

View File

@ -0,0 +1,347 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID(N'dbo.RelationshipCategories', N'U') IS NULL
BEGIN
CREATE TABLE dbo.RelationshipCategories
(
RelationshipCategoryID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_RelationshipCategories PRIMARY KEY,
CategoryName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_RelationshipCategories_IsActive DEFAULT 1,
CONSTRAINT UQ_RelationshipCategories_CategoryName UNIQUE(CategoryName)
);
END;
GO
MERGE dbo.RelationshipCategories AS target
USING (VALUES
(N'Family', 10),
(N'Friendship', 20),
(N'Romantic / Sexual', 30),
(N'Hostile / Conflict', 40),
(N'Professional / Social', 50),
(N'Other', 900)
) AS source (CategoryName, SortOrder)
ON target.CategoryName = source.CategoryName
WHEN MATCHED THEN UPDATE SET SortOrder = source.SortOrder, IsActive = 1
WHEN NOT MATCHED THEN INSERT (CategoryName, SortOrder) VALUES (source.CategoryName, source.SortOrder);
GO
IF COL_LENGTH(N'dbo.RelationshipTypes', N'RelationshipCategoryID') IS NULL
BEGIN
ALTER TABLE dbo.RelationshipTypes
ADD RelationshipCategoryID int NULL;
END;
GO
IF COL_LENGTH(N'dbo.RelationshipTypes', N'RelationshipCategoryID') IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_RelationshipTypes_Categories')
BEGIN
ALTER TABLE dbo.RelationshipTypes
ADD CONSTRAINT FK_RelationshipTypes_Categories FOREIGN KEY (RelationshipCategoryID) REFERENCES dbo.RelationshipCategories(RelationshipCategoryID);
END;
GO
UPDATE rt
SET RelationshipCategoryID = rc.RelationshipCategoryID
FROM dbo.RelationshipTypes rt
INNER JOIN
(
VALUES
(N'Parent', N'Family'), (N'Child', N'Family'), (N'Sibling', N'Family'), (N'Half-sibling', N'Family'),
(N'Adoptive Parent', N'Family'), (N'Biological Parent', N'Family'), (N'Guardian', N'Family'),
(N'Mother', N'Family'), (N'Father', N'Family'), (N'Sister', N'Family'), (N'Brother', N'Family'),
(N'Cousin', N'Family'), (N'Aunt', N'Family'), (N'Uncle', N'Family'),
(N'Friend', N'Friendship'), (N'Best Friend', N'Friendship'), (N'Ally', N'Friendship'), (N'Protector', N'Friendship'),
(N'Lover', N'Romantic / Sexual'), (N'Ex-lover', N'Romantic / Sexual'), (N'Spouse', N'Romantic / Sexual'),
(N'In Love', N'Romantic / Sexual'), (N'Romantic', N'Romantic / Sexual'), (N'Sexual Tension', N'Romantic / Sexual'),
(N'Enemy', N'Hostile / Conflict'), (N'Rival', N'Hostile / Conflict'), (N'Hostile', N'Hostile / Conflict'),
(N'Suspicious Of', N'Hostile / Conflict'), (N'Afraid Of', N'Hostile / Conflict'), (N'Estranged From', N'Hostile / Conflict'),
(N'Manipulating', N'Hostile / Conflict'), (N'Distrustful', N'Hostile / Conflict'),
(N'Colleague', N'Professional / Social'), (N'Employer', N'Professional / Social'), (N'Employee', N'Professional / Social'),
(N'Boss', N'Professional / Social'), (N'Acquaintance', N'Professional / Social'), (N'Neighbour', N'Professional / Social'),
(N'Landlord', N'Professional / Social'), (N'Tenant', N'Professional / Social'), (N'Teacher', N'Professional / Social'),
(N'Student', N'Professional / Social'),
(N'Unknown', N'Other'), (N'Stranger', N'Other'), (N'Other', N'Other')
) AS mapping (TypeName, CategoryName) ON mapping.TypeName = rt.TypeName
INNER JOIN dbo.RelationshipCategories rc ON rc.CategoryName = mapping.CategoryName;
GO
UPDATE rt
SET RelationshipCategoryID = otherCategory.RelationshipCategoryID
FROM dbo.RelationshipTypes rt
CROSS JOIN (SELECT RelationshipCategoryID FROM dbo.RelationshipCategories WHERE CategoryName = N'Other') otherCategory
WHERE rt.RelationshipCategoryID IS NULL;
GO
CREATE OR ALTER PROCEDURE dbo.RelationshipCategory_List
AS
BEGIN
SET NOCOUNT ON;
SELECT RelationshipCategoryID, CategoryName, SortOrder, IsActive
FROM dbo.RelationshipCategories
WHERE IsActive = 1
ORDER BY SortOrder, CategoryName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.RelationshipType_ListWithCategories
AS
BEGIN
SET NOCOUNT ON;
SELECT rt.RelationshipTypeID, rt.TypeName, rt.IsPermanentDefault, rt.SortOrder, rt.IsActive,
rt.RelationshipCategoryID, rc.CategoryName AS RelationshipCategoryName, rc.SortOrder AS CategorySortOrder
FROM dbo.RelationshipTypes rt
LEFT JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID
ORDER BY COALESCE(rc.SortOrder, 900), rt.SortOrder, rt.TypeName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.RelationshipType_UpdateCategory
@RelationshipTypeID int,
@TypeName nvarchar(100),
@RelationshipCategoryID int,
@SortOrder int,
@IsActive bit
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.RelationshipTypes
SET TypeName = @TypeName,
RelationshipCategoryID = @RelationshipCategoryID,
SortOrder = @SortOrder,
IsActive = @IsActive
WHERE RelationshipTypeID = @RelationshipTypeID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterLookup_All
AS
BEGIN
SET NOCOUNT ON;
SELECT CharacterRoleInSceneTypeID, TypeName, SortOrder, IsActive FROM dbo.CharacterRoleInSceneTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT PresenceTypeID, TypeName, SortOrder, IsActive FROM dbo.PresenceTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT CharacterAttributeTypeID, ProjectID, AttributeName, DataType, IsNormallyStable, SortOrder, IsActive FROM dbo.CharacterAttributeTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT KnowledgeStateID, StateName, SortOrder, IsActive FROM dbo.KnowledgeStates WHERE IsActive = 1 ORDER BY SortOrder;
SELECT rt.RelationshipTypeID, rt.TypeName, rt.IsPermanentDefault, rt.SortOrder, rt.IsActive,
rt.RelationshipCategoryID, rc.CategoryName AS RelationshipCategoryName, rc.SortOrder AS CategorySortOrder
FROM dbo.RelationshipTypes rt
LEFT JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID
WHERE rt.IsActive = 1
ORDER BY COALESCE(rc.SortOrder, 900), rt.SortOrder, rt.TypeName;
SELECT RelationshipStateID, StateName, SortOrder, IsActive FROM dbo.RelationshipStates WHERE IsActive = 1 ORDER BY SortOrder;
SELECT RelationshipCategoryID, CategoryName, SortOrder, IsActive FROM dbo.RelationshipCategories WHERE IsActive = 1 ORDER BY SortOrder, CategoryName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Relationship_ListInitialByCharacter
@CharacterID int
AS
BEGIN
SET NOCOUNT ON;
SELECT cr.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, ca.CharacterName AS CharacterAName,
cr.CharacterBID, cb.CharacterName AS CharacterBName, cr.RelationshipTypeID, rt.TypeName AS RelationshipTypeName,
rt.RelationshipCategoryID, rc.CategoryName AS RelationshipCategoryName, COALESCE(rc.SortOrder, 900) AS CategorySortOrder,
rt.SortOrder AS RelationshipTypeSortOrder,
cr.IsPermanent, cr.StartSceneID, cr.EndSceneID, cr.IsKnownToReader, cr.Notes,
cr.IsInitialRelationship, cr.InitialBookID, b.BookTitle AS InitialBookTitle, cr.ReaderInitiallyKnows,
cr.InitialRelationshipStateID, rs.StateName AS InitialRelationshipStateName, cr.InitialIntensity, cr.IsReciprocal,
cr.CreatedDate, cr.UpdatedDate, cr.IsArchived
FROM dbo.CharacterRelationships cr
INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID
INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID
INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID
LEFT JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID
LEFT JOIN dbo.Books b ON b.BookID = cr.InitialBookID
LEFT JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = cr.InitialRelationshipStateID
WHERE cr.IsArchived = 0
AND cr.IsInitialRelationship = 1
AND @CharacterID IN (cr.CharacterAID, cr.CharacterBID)
ORDER BY COALESCE(rc.SortOrder, 900), COALESCE(b.SortOrder, 0), rt.SortOrder, ca.CharacterName, cb.CharacterName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Relationship_ListByCharacter
@CharacterID int
AS
BEGIN
SET NOCOUNT ON;
SELECT cr.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, ca.CharacterName AS CharacterAName,
cr.CharacterBID, cb.CharacterName AS CharacterBName, cr.RelationshipTypeID, rt.TypeName AS RelationshipTypeName,
rt.RelationshipCategoryID, rc.CategoryName AS RelationshipCategoryName, COALESCE(rc.SortOrder, 900) AS CategorySortOrder,
rt.SortOrder AS RelationshipTypeSortOrder,
cr.IsPermanent, cr.StartSceneID, cr.EndSceneID, cr.IsKnownToReader, cr.Notes,
cr.IsInitialRelationship, cr.InitialBookID, b.BookTitle AS InitialBookTitle, cr.ReaderInitiallyKnows,
cr.InitialRelationshipStateID, rs.StateName AS InitialRelationshipStateName, cr.InitialIntensity, cr.IsReciprocal,
cr.CreatedDate, cr.UpdatedDate, cr.IsArchived
FROM dbo.CharacterRelationships cr
INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID
INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID
INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID
LEFT JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID
LEFT JOIN dbo.Books b ON b.BookID = cr.InitialBookID
LEFT JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = cr.InitialRelationshipStateID
WHERE cr.IsArchived = 0 AND @CharacterID IN (cr.CharacterAID, cr.CharacterBID)
ORDER BY COALESCE(rc.SortOrder, 900), ca.CharacterName, cb.CharacterName, rt.SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Relationship_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT cr.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, ca.CharacterName AS CharacterAName,
cr.CharacterBID, cb.CharacterName AS CharacterBName, cr.RelationshipTypeID, rt.TypeName AS RelationshipTypeName,
rt.RelationshipCategoryID, rc.CategoryName AS RelationshipCategoryName, COALESCE(rc.SortOrder, 900) AS CategorySortOrder,
rt.SortOrder AS RelationshipTypeSortOrder,
cr.IsPermanent, cr.StartSceneID, cr.EndSceneID, cr.IsKnownToReader, cr.Notes,
cr.IsInitialRelationship, cr.InitialBookID, b.BookTitle AS InitialBookTitle, cr.ReaderInitiallyKnows,
cr.InitialRelationshipStateID, rs.StateName AS InitialRelationshipStateName, cr.InitialIntensity, cr.IsReciprocal,
cr.CreatedDate, cr.UpdatedDate, cr.IsArchived
FROM dbo.CharacterRelationships cr
INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID
INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID
INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID
LEFT JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID
LEFT JOIN dbo.Books b ON b.BookID = cr.InitialBookID
LEFT JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = cr.InitialRelationshipStateID
WHERE cr.ProjectID = @ProjectID AND cr.IsArchived = 0
ORDER BY COALESCE(rc.SortOrder, 900), ca.CharacterName, cb.CharacterName, rt.SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.RelationshipEvent_ListByCharacter
@CharacterID int
AS
BEGIN
SET NOCOUNT ON;
SELECT re.RelationshipEventID, re.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, ca.CharacterName AS CharacterAName,
cr.CharacterBID, cb.CharacterName AS CharacterBName, rt.TypeName AS RelationshipTypeName,
rt.RelationshipCategoryID, rc.CategoryName AS RelationshipCategoryName, COALESCE(rc.SortOrder, 900) AS CategorySortOrder,
rt.SortOrder AS RelationshipTypeSortOrder,
cr.IsReciprocal, re.SceneID, s.SceneNumber, s.SceneTitle, ch.ChapterNumber, b.BookTitle,
re.RelationshipStateID, rs.StateName AS RelationshipStateName, re.Intensity, re.IsKnownToOtherCharacter,
re.Description, re.CreatedDate, re.UpdatedDate
FROM dbo.RelationshipEvents re
INNER JOIN dbo.CharacterRelationships cr ON cr.CharacterRelationshipID = re.CharacterRelationshipID
INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID
INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID
INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID
LEFT JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID
INNER JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = re.RelationshipStateID
INNER JOIN dbo.Scenes s ON s.SceneID = re.SceneID
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = ch.BookID
WHERE @CharacterID IN (cr.CharacterAID, cr.CharacterBID)
ORDER BY b.SortOrder, ch.SortOrder, s.SortOrder, re.CreatedDate, re.RelationshipEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.RelationshipEvent_ListByScene
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
SELECT re.RelationshipEventID, re.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, ca.CharacterName AS CharacterAName,
cr.CharacterBID, cb.CharacterName AS CharacterBName, rt.TypeName AS RelationshipTypeName,
rt.RelationshipCategoryID, rc.CategoryName AS RelationshipCategoryName, COALESCE(rc.SortOrder, 900) AS CategorySortOrder,
rt.SortOrder AS RelationshipTypeSortOrder,
cr.IsReciprocal, re.SceneID, re.RelationshipStateID, rs.StateName AS RelationshipStateName, re.Intensity, re.IsKnownToOtherCharacter,
re.Description, re.CreatedDate, re.UpdatedDate
FROM dbo.RelationshipEvents re
INNER JOIN dbo.CharacterRelationships cr ON cr.CharacterRelationshipID = re.CharacterRelationshipID
INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID
INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID
INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID
LEFT JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID
INNER JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = re.RelationshipStateID
WHERE re.SceneID = @SceneID
ORDER BY ca.CharacterName, cb.CharacterName, re.CreatedDate, re.RelationshipEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.StoryBible_RelationshipSummaries
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT cr.CharacterRelationshipID,
cr.CharacterAID,
ca.CharacterName AS CharacterAName,
cr.CharacterBID,
cb.CharacterName AS CharacterBName,
rt.TypeName AS RelationshipTypeName,
rt.RelationshipCategoryID,
rc.CategoryName AS RelationshipCategoryName,
COALESCE(rc.SortOrder, 900) AS CategorySortOrder,
rt.SortOrder AS RelationshipTypeSortOrder,
CASE WHEN cr.IsInitialRelationship = 1 THEN rt.TypeName ELSE NULL END AS InitialRelationshipName,
initialState.StateName AS InitialStateName,
cr.InitialIntensity,
COALESCE(currentEvent.StateName, initialState.StateName, rt.TypeName) AS CurrentStateName,
COALESCE(currentEvent.Intensity, cr.InitialIntensity) AS CurrentIntensity,
ISNULL(events.EventCount, 0) AS EventCount,
firstEvent.SceneLabel AS FirstEventLabel,
events.EventHistory
FROM dbo.CharacterRelationships cr
INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID
INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID
INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID
LEFT JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID
LEFT JOIN dbo.RelationshipStates initialState ON initialState.RelationshipStateID = cr.InitialRelationshipStateID
OUTER APPLY
(
SELECT TOP (1) rs.StateName, re.Intensity
FROM dbo.RelationshipEvents re
INNER JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = re.RelationshipStateID
INNER JOIN dbo.Scenes s ON s.SceneID = re.SceneID AND s.IsArchived = 0
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID AND c.IsArchived = 0
INNER JOIN dbo.Books b ON b.BookID = c.BookID AND b.IsArchived = 0
WHERE re.CharacterRelationshipID = cr.CharacterRelationshipID
ORDER BY b.SortOrder DESC, c.SortOrder DESC, s.SortOrder DESC, re.CreatedDate DESC, re.RelationshipEventID DESC
) currentEvent
OUTER APPLY
(
SELECT COUNT(*) AS EventCount,
STRING_AGG(
CONVERT(nvarchar(max), CONCAT(b.BookTitle, N' / Ch ', c.ChapterNumber, N' / Scene ', s.SceneNumber, N': ', rs.StateName, N' [', ISNULL(eventCategory.CategoryName, N'Other'), N']', ISNULL(N' (' + CONVERT(nvarchar(20), re.Intensity) + N')', N''), ISNULL(N' - ' + re.Description, N''))),
N' -> '
) WITHIN GROUP (ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, re.CreatedDate, re.RelationshipEventID) AS EventHistory
FROM dbo.RelationshipEvents re
INNER JOIN dbo.CharacterRelationships eventRelationship ON eventRelationship.CharacterRelationshipID = re.CharacterRelationshipID
INNER JOIN dbo.RelationshipTypes eventType ON eventType.RelationshipTypeID = eventRelationship.RelationshipTypeID
LEFT JOIN dbo.RelationshipCategories eventCategory ON eventCategory.RelationshipCategoryID = eventType.RelationshipCategoryID
INNER JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = re.RelationshipStateID
INNER JOIN dbo.Scenes s ON s.SceneID = re.SceneID AND s.IsArchived = 0
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID AND c.IsArchived = 0
INNER JOIN dbo.Books b ON b.BookID = c.BookID AND b.IsArchived = 0
WHERE re.CharacterRelationshipID = cr.CharacterRelationshipID
) events
OUTER APPLY
(
SELECT TOP (1) CONCAT(b.BookTitle, N' / Ch ', c.ChapterNumber, N' / Scene ', s.SceneNumber, N': ', s.SceneTitle) AS SceneLabel
FROM dbo.RelationshipEvents re
INNER JOIN dbo.Scenes s ON s.SceneID = re.SceneID AND s.IsArchived = 0
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID AND c.IsArchived = 0
INNER JOIN dbo.Books b ON b.BookID = c.BookID AND b.IsArchived = 0
WHERE re.CharacterRelationshipID = cr.CharacterRelationshipID
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, re.CreatedDate, re.RelationshipEventID
) firstEvent
WHERE cr.ProjectID = @ProjectID AND cr.IsArchived = 0
ORDER BY COALESCE(rc.SortOrder, 900), ca.CharacterName, cb.CharacterName;
END;
GO

View File

@ -77,13 +77,16 @@
else
{
<div class="asset-mini-list">
@foreach (var relationship in Model.InitialRelationships)
@foreach (var categoryGroup in Model.InitialRelationships.GroupBy(x => new { x.RelationshipCategoryName, x.CategorySortOrder }).OrderBy(x => x.Key.CategorySortOrder).ThenBy(x => x.Key.RelationshipCategoryName))
{
<h3 class="eyebrow">@categoryGroup.Key.RelationshipCategoryName</h3>
@foreach (var relationship in categoryGroup.OrderBy(x => x.RelationshipTypeSortOrder).ThenBy(x => x.CharacterAName).ThenBy(x => x.CharacterBName))
{
var initialSourceId = relationship.CharacterAID;
var initialRelatedId = relationship.CharacterBID;
<div class="asset-mini-item">
<strong>@relationship.CharacterAName -> @relationship.CharacterBName</strong>
<span>@relationship.RelationshipTypeName</span>
<span>@relationship.RelationshipTypeName <span class="status-pill">@relationship.RelationshipCategoryName</span></span>
<span>Reciprocal: @(relationship.IsReciprocal ? "Yes" : "No")</span>
<span>Reader initially knows: @(relationship.ReaderInitiallyKnows ? "Yes" : "No")</span>
@if (!string.IsNullOrWhiteSpace(relationship.InitialRelationshipStateName) || relationship.InitialIntensity.HasValue)
@ -177,6 +180,7 @@
</form>
</div>
}
}
</div>
}
<details class="mt-3">
@ -248,15 +252,18 @@
else
{
<div class="asset-mini-list">
@foreach (var current in Model.CurrentRelationships)
@foreach (var categoryGroup in Model.CurrentRelationships.GroupBy(x => new { x.Relationship.RelationshipCategoryName, x.Relationship.CategorySortOrder }).OrderBy(x => x.Key.CategorySortOrder).ThenBy(x => x.Key.RelationshipCategoryName))
{
<h3 class="eyebrow">@categoryGroup.Key.RelationshipCategoryName</h3>
@foreach (var current in categoryGroup.OrderBy(x => x.Relationship.RelationshipTypeSortOrder).ThenBy(x => x.Relationship.CharacterAName).ThenBy(x => x.Relationship.CharacterBName))
{
var relationship = current.Relationship;
var relatedCharacterId = relationship.CharacterAID == Model.Character.CharacterID ? relationship.CharacterBID : relationship.CharacterAID;
var relatedCharacterName = relationship.CharacterAID == Model.Character.CharacterID ? relationship.CharacterBName : relationship.CharacterAName;
<div class="asset-mini-item">
<strong>@relatedCharacterName</strong>
<span>@relationship.RelationshipTypeName · Current: @current.CurrentStateName</span>
<span>Reciprocal: @(relationship.IsReciprocal ? "Yes" : "No") · Confidence / intensity: @(current.CurrentIntensity?.ToString() ?? "Unknown")</span>
<span>@relationship.RelationshipTypeName <span class="status-pill">@relationship.RelationshipCategoryName</span> / Current: @current.CurrentStateName</span>
<span>Reciprocal: @(relationship.IsReciprocal ? "Yes" : "No") / Confidence / intensity: @(current.CurrentIntensity?.ToString() ?? "Unknown")</span>
@if (!string.IsNullOrWhiteSpace(current.CurrentNotes))
{
<span>@current.CurrentNotes</span>
@ -267,8 +274,8 @@
<summary>View History</summary>
<div class="asset-mini-list mt-2">
<div class="asset-mini-item">
<strong>Initial: @relationship.RelationshipTypeName</strong>
<span>@(relationship.InitialRelationshipStateName ?? "No awareness state")@if (relationship.InitialIntensity.HasValue) { <text> · Intensity @relationship.InitialIntensity</text> }</span>
<strong>Initial: @relationship.RelationshipTypeName <span class="status-pill">@relationship.RelationshipCategoryName</span></strong>
<span>@(relationship.InitialRelationshipStateName ?? "No awareness state")@if (relationship.InitialIntensity.HasValue) { <text> / Intensity @relationship.InitialIntensity</text> }</span>
<span>Reader initially knows: @(relationship.ReaderInitiallyKnows ? "Yes" : "No")</span>
@if (!string.IsNullOrWhiteSpace(relationship.Notes))
{
@ -279,7 +286,7 @@
{
<div class="asset-mini-item">
<strong>@item.BookTitle / Ch @item.ChapterNumber / Scene @item.SceneNumber: @item.SceneTitle</strong>
<span>@item.RelationshipStateName · Intensity @(item.Intensity?.ToString() ?? "Unknown")</span>
<span>@item.RelationshipStateName <span class="status-pill">@item.RelationshipCategoryName</span> / Intensity @(item.Intensity?.ToString() ?? "Unknown")</span>
<span>Awareness: @(item.IsKnownToOtherCharacter ? "known to other character" : "not known to other character")</span>
@if (!string.IsNullOrWhiteSpace(item.Description))
{
@ -296,10 +303,10 @@
</details>
</div>
}
}
</div>
}
</section>
<section class="list-section mt-3">
<h2>Relationship Events</h2>
@if (!Model.RelationshipEvents.Any())
@ -314,8 +321,8 @@
<div class="asset-mini-item">
<strong>@item.CharacterAName -> @item.CharacterBName: @item.RelationshipStateName</strong>
<span>@item.BookTitle / Ch @item.ChapterNumber / Scene @item.SceneNumber: @item.SceneTitle</span>
<span>@item.RelationshipTypeName · Awareness: @(item.IsKnownToOtherCharacter ? "known to other character" : "not known to other character")</span>
<span>Reciprocal: @(item.IsReciprocal ? "Yes" : "No") · Confidence / intensity: @(item.Intensity?.ToString() ?? "Unknown")</span>
<span>@item.RelationshipTypeName <span class="status-pill">@item.RelationshipCategoryName</span> / Awareness: @(item.IsKnownToOtherCharacter ? "known to other character" : "not known to other character")</span>
<span>Reciprocal: @(item.IsReciprocal ? "Yes" : "No") / Confidence / intensity: @(item.Intensity?.ToString() ?? "Unknown")</span>
@if (!string.IsNullOrWhiteSpace(item.Description))
{
<span>@item.Description</span>
@ -394,3 +401,5 @@
}
</section>

View File

@ -237,17 +237,18 @@
<span class="soft-count">@Model.Relationships.Count relationships</span>
</div>
<div class="story-bible-grid">
@foreach (var group in relationshipsByCharacter)
@foreach (var categoryGroup in Model.Relationships.GroupBy(x => new { x.RelationshipCategoryName, x.CategorySortOrder }).OrderBy(x => x.Key.CategorySortOrder).ThenBy(x => x.Key.RelationshipCategoryName))
{
<article class="story-bible-card">
<h3>@group.Key.CharacterAName</h3>
<h3>@categoryGroup.Key.RelationshipCategoryName</h3>
<ul class="story-bible-link-list">
@foreach (var relationship in group)
@foreach (var relationship in categoryGroup.OrderBy(x => x.CharacterAName).ThenBy(x => x.CharacterBName).ThenBy(x => x.RelationshipTypeSortOrder))
{
<li>
<a asp-controller="Characters" asp-action="Details" asp-route-id="@relationship.CharacterBID">@relationship.CharacterBName</a>
<span>Initial: @(relationship.InitialRelationshipName ?? relationship.RelationshipTypeName)@if (!string.IsNullOrWhiteSpace(relationship.InitialStateName)) { <text> / @relationship.InitialStateName</text> }@if (relationship.InitialIntensity.HasValue) { <text> · Intensity @relationship.InitialIntensity</text> }</span>
<span>Current: @(relationship.CurrentStateName ?? "No current state")@if (relationship.CurrentIntensity.HasValue) { <text> · Intensity @relationship.CurrentIntensity</text> }</span>
<a asp-controller="Characters" asp-action="Details" asp-route-id="@relationship.CharacterBID">@relationship.CharacterAName / @relationship.CharacterBName</a>
<span>@relationship.RelationshipTypeName <span class="status-pill">@relationship.RelationshipCategoryName</span></span>
<span>Initial: @(relationship.InitialRelationshipName ?? relationship.RelationshipTypeName)@if (!string.IsNullOrWhiteSpace(relationship.InitialStateName)) { <text> / @relationship.InitialStateName</text> }@if (relationship.InitialIntensity.HasValue) { <text> / Intensity @relationship.InitialIntensity</text> }</span>
<span>Current: @(relationship.CurrentStateName ?? "No current state")@if (relationship.CurrentIntensity.HasValue) { <text> / Intensity @relationship.CurrentIntensity</text> }</span>
@if (!string.IsNullOrWhiteSpace(relationship.EventHistory))
{
<span>Relationship Event History: @relationship.EventHistory</span>
@ -259,7 +260,6 @@
}
</div>
</section>
<section class="story-bible-section" id="locations">
<div class="story-bible-section-heading">
<div>
@ -404,3 +404,4 @@
};
}
}