gtvm-hs-1.0.0: src/GTVM/Studio.hs
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
{- Notes
* SCPTL comments are fully ignored. They aren't indexable. They should be
inside the constructors instead.
* We ignore SCPTL source fields! Use the SCP instead. We can fill them all in
in a separate pass e.g. before exporting.
TODO
* handling unexpected index errors with runtime errors. Perhaps upgrade to
explicitly handled ones via effect. (Impurely, they can be propagated simply
with the final IO interpreter.)
-}
module GTVM.Studio where
import GTVM.SCP.TL
import GTVM.SCP.TL qualified as Scptl
import GTVM.SCP
import Strongweak
import Polysemy
import Polysemy.State
import Polysemy.State qualified as Polysemy
import Polysemy.Output
import Polysemy.Output qualified as Polysemy
import Path qualified
import Path ( Path, Rel, Abs, File, Dir, (</>) )
import Data.Text qualified as Text
import Data.Text.IO qualified as Text
import Data.Text ( Text )
import Numeric.Natural
import Control.Monad
import Control.Monad.Catch
import Control.Monad.IO.Class
import Data.Functor.Const
import Data.Yaml qualified as Yaml
import GHC.Generics ( Generic )
import Data.String ( IsString )
newtype ScpId = ScpId { getScpId :: Text }
deriving (Eq, IsString) via Text
deriving stock Show
-- | Yes, we're ignoring the source fields. We'll fill them in during an export
-- pass. Easier on the brain.
type TLSeg' = TLSeg (Const ()) Text
data Studio m a where
WriteTl :: TLSeg' -> Studio m ()
ReadTl :: Studio m (Maybe TLSeg')
-- ^ 'Nothing' if no entry yet. (Perhaps upgrade 'Data.Text.empty' to
-- 'Nothing'.)
JumpTlInit :: Studio m ()
-- ^ Jump to the first translation target SCP command and reset.
NextTl :: Studio m ()
-- ^ Step to the next translation target SCP command.
PrevTl :: Studio m ()
-- ^ Step to the previous translation target SCP command.
LoadScp :: ScpId -> Studio m ()
-- ^ Load the requested SCP.
GenerateFreshScpTl :: Path Rel Dir -> Studio m ()
-- ^ Generate a fresh SCPTL for the currently loaded SCP and place in the
-- the requested studio folder.
LoadScpTl :: Path Rel Dir -> Studio m ()
-- ^ Load the SCPTL for the currently loaded SCP, stored at the requested
-- studio folder.
SaveScpTl :: Path Rel Dir -> Studio m ()
-- ^ Save the currently loaded SCPTL to disk.
makeSem ''Studio
-- | Resets and steps to the translation target SCP command with the requested
-- index.
--
-- TODO better behaviour if we jump too far (don't keep spamming 'nextTl')? but
-- this should be prefaced with a UI check that the index exists
jumpTl :: Members '[Studio] r => Natural -> Sem r ()
jumpTl n = do
jumpTlInit
replicateM_ (fromIntegral n) nextTl
-- | Generate a fresh SCPTL for the currently loaded SCP, place in the requested
-- studio folder and load immediately.
loadFreshScpTl :: Members '[Studio] r => Path Rel Dir -> Sem r ()
loadFreshScpTl d = generateFreshScpTl d >> loadScpTl d
data St = St
{ stScp :: [Seg 'Weak Text]
, stScpIdx :: Int
, stScptl :: Maybe [TLSeg']
, stScptlIdx :: Int
, stScpId :: ScpId
} deriving stock (Generic, Show, Eq)
setAt :: Int -> a -> [a] -> [a]
setAt i a ls
| i < 0 = ls
| otherwise = go i ls
where
go 0 (_:xs) = a : xs
go n (x:xs) = x : go (n-1) xs
go _ [] = []
runStudio
:: Members '[State St, Output Text, Embed IO] r
=> Path Abs Dir -> Sem (Studio ': r) a -> Sem r a
runStudio baseDir = interpret $ \case
WriteTl tlseg -> do
st <- get
case stScptl st of
Nothing ->
studioLog "no SCPTL loaded, can't write segment TL"
Just scptl -> do
let scptl' = setAt (stScptlIdx st) tlseg scptl
put st { stScptl = Just scptl' }
ReadTl -> do
st <- get
case stScptl st of
Nothing -> do
studioLog "no SCPTL loaded, can't read current segment TL"
pure Nothing
Just scptl ->
let tlSeg = scptl !! stScptlIdx st
in if tlSegIsEmpty tlSeg then pure Nothing else pure (Just tlSeg)
JumpTlInit -> do
st <- get
put st { stScpIdx = 0, stScptlIdx = 0 }
NextTl -> do -- TODO would benefit from lenses
st <- get
let scp = stScp st
scpIdx = stScpIdx st
if scpIdx >= length scp - 1 then
-- need more than check to handle length 0,1 cases
studioLog "at SCP end, can't step any further"
else do
let scptlIdx = stScptlIdx st
(scpIdx', scptlIdx') =
case scpNextTlIdx scp (scpIdx+1) of
Left scpIdx'' -> (scpIdx'', scptlIdx)
Right scpIdx'' -> (scpIdx'', scptlIdx+1)
st' = st { stScpIdx = scpIdx'
, stScptlIdx = scptlIdx' }
put st'
PrevTl -> do -- TODO would benefit from lenses
st <- get
let scp = stScp st
scpIdx = stScpIdx st
if scpIdx == 0 then
studioLog "at SCP start, can't rewind any further"
else do
let scptlIdx = stScptlIdx st
(scpIdx', scptlIdx') =
case scpPrevTlIdx scp (scpIdx-1) of
Nothing -> (0, scptlIdx)
Just scpIdx'' -> (scpIdx'', scptlIdx-1)
st' = st { stScpIdx = scpIdx'
, stScptlIdx = scptlIdx' }
put st'
LoadScp scpId -> do
scpFName <- liftIO $ studioYamlRes ".scp" $ getScpId scpId
let scpFPath = $(Path.mkRelDir "scp") </> scpFName
fpath = baseDir </> scpFPath
fpath' = Path.fromAbsFile fpath
scp <- Yaml.decodeFileThrow fpath'
modify $ \st -> st { stScpIdx = 0, stScp = scp, stScpId = scpId }
GenerateFreshScpTl d -> do
scp <- gets stScp
scpId <- gets stScpId
scptlFName <- liftIO $ studioYamlRes ".scptl" $ getScpId scpId
let scptlFPath = $(Path.mkRelDir "scptl") </> d </> scptlFName
fpath = baseDir </> scptlFPath
fpath' = Path.fromAbsFile fpath
let scptl = genEmptyScptl scp
liftIO $ Yaml.encodeFile fpath' scptl
LoadScpTl d -> do
scpId <- gets stScpId
scptlFName <- liftIO $ studioYamlRes ".scptl" $ getScpId scpId
let scptlFPath = $(Path.mkRelDir "scptl") </> d </> scptlFName
fpath = baseDir </> scptlFPath
fpath' = Path.fromAbsFile fpath
scptl <- Yaml.decodeFileThrow fpath'
modify $ \st -> st { stScptl = Just scptl }
SaveScpTl d -> do
st <- get
case stScptl st of
Nothing -> pure () -- TODO notify
Just scptl -> do
scpId <- gets stScpId
scptlFName <- liftIO $ studioYamlRes ".scptl" $ getScpId scpId
let scptlFPath = $(Path.mkRelDir "scptl") </> d </> scptlFName
fpath = baseDir </> scptlFPath
fpath' = Path.fromAbsFile fpath
liftIO $ Yaml.encodeFile fpath' scptl
studioYamlRes :: MonadThrow m => FilePath -> Text -> m (Path Rel File)
studioYamlRes ext res = Path.parseRelFile $ Text.unpack res<>ext<>".yaml"
scpPrevTlIdx :: SCP f a -> Int -> Maybe Int
scpPrevTlIdx scp = go
where
go = \case
-1 -> Nothing
scpIdx -> if segIsTlTarget (scp !! scpIdx)
then Just scpIdx
else go $ scpIdx-1
scpNextTlIdx :: SCP f a -> Int -> Either Int Int
scpNextTlIdx scp = go
where
l = length scp
go scpIdx =
if scpIdx >= l-1
then Left $ scpIdx-1
else if segIsTlTarget (scp !! scpIdx)
then Right scpIdx
else go $ scpIdx+1
tlSegIsEmpty :: (Eq a, Monoid a) => TLSeg f a -> Bool
tlSegIsEmpty = \case
TLSegTextbox' x -> tlSegTextboxTranslation x == mempty -- TODO check overflow
_ -> False
-- | Required to avoid lots of type annotation, that's life Jim
studioLog :: Member (Output Text) r => Text -> Sem r ()
studioLog = Polysemy.output
genEmptyScptl :: SCP 'Weak Text -> [TLSeg']
genEmptyScptl = map Scptl.segDropMeta . filter (not . isComment) . Scptl.genTL env
where
env = Scptl.Env mempty (const Nothing)
isComment = \case TLSegComment'{} -> True; _ -> False
rLoadAndGenFirstScript :: IO ()
rLoadAndGenFirstScript =
Polysemy.runFinal
. Polysemy.embedToFinal @IO
. runStudioLog Text.putStrLn
. Polysemy.evalState (St [] 0 Nothing 0 "")
. runStudio $(Path.mkAbsDir "/home/raehik/sh/gtvm-tl/tooling/gtvm-hs/tmp/studio")
$ pLoadAndGenFirstScript
--------------------------------------------------------------------------------
runStudioLog
:: Member (Embed IO) r
=> (o -> IO ()) -> Sem (Output o ': r) a -> Sem r a
runStudioLog f = interpret $ \case
Output o -> liftIO $ f o
--------------------------------------------------------------------------------
pLoadAndGenFirstScript :: Members '[Studio, Embed IO] r => Sem r ()
pLoadAndGenFirstScript = do
loadScp "00120zzz0"
generateFreshScpTl $(Path.mkRelDir "tmp")
loadScpTl $(Path.mkRelDir "tmp")
readTl >>= liftIO . print
writeTl $ tlsegTextbox "ayy lmao"
readTl >>= liftIO . print
nextTl
nextTl
writeTl $ tlsegTextbox "two ahead"
saveScpTl $(Path.mkRelDir "tmp")
tlsegTextbox :: a -> TLSeg (Const ()) a
tlsegTextbox a = Scptl.TLSegTextbox' $ Scptl.TLSegTextbox (Const ()) a Nothing