immutaball-core 0.1.0.4.1 → 0.1.0.5.1
raw patch · 33 files changed
+3963/−1329 lines, 33 files
Files
- CHANGELOG.md +7/−0
- Data/LabeledBinTree.hs +460/−0
- Immutaball/Ball/CLI.hs +106/−6
- Immutaball/Ball/CLI/Config.hs +19/−12
- Immutaball/Ball/Game.hs +1/−0
- Immutaball/Ball/Level.hs +17/−0
- Immutaball/Ball/Level/Render.hs +265/−0
- Immutaball/Ball/State/Game.hs +1953/−908
- Immutaball/Ball/State/Play.hs +2/−1
- Immutaball/Putt/CLI.hs +1/−1
- Immutaball/Share/Config.hs +8/−2
- Immutaball/Share/Level.hs +0/−2
- Immutaball/Share/Level/Analysis.hs +406/−9
- Immutaball/Share/Level/Render.hs +0/−265
- Immutaball/Share/Level/Utils.hs +16/−1
- Immutaball/Share/Math/Core.hs +76/−7
- Immutaball/Share/Math/X3D.hs +73/−3
- Immutaball/Share/Utils.hs +69/−1
- Main.hs +2/−1
- NOTES-building-dependencies.md +97/−0
- Putt.hs +2/−1
- README.md +9/−95
- Test.hs +2/−1
- Test/Data/LabeledBinTree/Orphans.hs +45/−0
- Test/Data/LabeledBinTree/Test.hs +79/−0
- Test/Immutaball/Share/Math/X3D/Test.hs +67/−3
- Test/Immutaball/Test.hs +2/−0
- dependency-substitutions/wires-fallback-meta/Control/Wire/Meta.hs +18/−0
- dependency-substitutions/wires-nofallback-meta/Control/Wire/Meta.hs +18/−0
- dependency-substitutions/wires/Control/Wire.hs +17/−0
- dependency-substitutions/wires/Control/Wire/Controller.hs +15/−0
- dependency-substitutions/wires/Control/Wire/Internal.hs +62/−0
- immutaball-core.cabal +49/−10
+ CHANGELOG.md view
@@ -0,0 +1,7 @@+# History of ‘immutaball’ versions.++## 0.1.0.5-dev++## 0.1.0.4 -- 2025-04-10++- Released Immutaball with minimal functionality.
+ Data/LabeledBinTree.hs view
@@ -0,0 +1,460 @@+{-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .+-- Enable warnings:+{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}++-- CLI.hs.++{-# LANGUAGE Haskell2010 #-}+{-# LANGUAGE TemplateHaskell, InstanceSigs #-}++module Data.LabeledBinTree+ (+ BinTree,+ BinTreeF(..),+ BinTreeLabeled,+ LabeledBinTree(..), labeledBinTree,+ Tree,+ deconsLabeledBinTree,+ mkLabeledEmpty,+ mkLabeledLeaf,+ mkLabeledFork,+ fmapLabeledBinTree,+ foldrLabeledBinTree,+ simplifyLeavesLabeledBinTree,+ simplifyEmptiesLabeledBinTree,+ normalizeLabeledBinTree,+ forkinizeLeaves,+ forkinizeLeavesDirect,+ joinLabeledBinTree,+ joinDirectLabeledBinTree,+ singletonLabeledBin,+ repeatLabeledBinTree,+ pureLabeledBinTreeLossy,+ appLabeledBinTreeLossy,+ labeledBinTreeConcatRightmost,+ pureLabeledBinTreeCombinatorial,+ appLabeledBinTreeCombinatorial,+ mapLeavesDirectLabeledBinTree,+ mapLeavesLabeledBinTree,+ mapNonleavesLabeledBinTree,+ bindLabeledBinTree,+ traverseLBT,++ -- * Sizes+ numElemsLBT,+ numNodesWeightedDirectLBT,+ numNodesDirectLBT,+ numEmptyDirectLBT,+ numLeavesDirectLBT,+ numForkDirectLBT,+ numAnyDirectLBT,++ numElemsLBTI,+ numNodesWeightedDirectLBTI,+ numNodesDirectLBTI,+ numEmptyDirectLBTI,+ numLeavesDirectLBTI,+ numForkDirectLBTI,+ numAnyDirectLBTI,++ -- * Constructor aliases+ emptyLBT,+ leafLBT,+ forkLBT,++ -- * Equivalence relations+ eqLBT,+ leqLBT,++ -- * Utils+ showFs,+ showFs'+ ) where++import Prelude ()+import Immutaball.Prelude++import Data.Function hiding (id, (.))++import Control.Lens++import Immutaball.Share.Utils++type BinTree n l = Fixed (BinTreeF n l)+data BinTreeF n l me =+ -- | Empty node.+ EmptyBT+ -- | Leaf node.+ | LeafBT l+ -- | Fork node.+ | ForkBT me n me+ deriving (Eq, Ord, Show)++type BinTreeLabeled a = BinTree a a++newtype LabeledBinTree a = LabeledBinTree {_labeledBinTree :: BinTreeLabeled a}+ deriving (Eq, Ord, Show)+makeLenses ''LabeledBinTree++type Tree = LabeledBinTree++deconsLabeledBinTree :: r -> (a -> r) -> (LabeledBinTree a -> a -> LabeledBinTree a -> r) -> LabeledBinTree a -> r+deconsLabeledBinTree withEmptyBT _ _ (LabeledBinTree (Fixed (EmptyBT ))) = withEmptyBT+deconsLabeledBinTree _ withLeafBT _ (LabeledBinTree (Fixed (LeafBT a ))) = withLeafBT a+deconsLabeledBinTree _ _ withForkBT (LabeledBinTree (Fixed (ForkBT l a r))) = withForkBT (LabeledBinTree l) a (LabeledBinTree r)++mkLabeledEmpty :: LabeledBinTree a+mkLabeledEmpty = LabeledBinTree . Fixed $ EmptyBT++mkLabeledLeaf :: a -> LabeledBinTree a+mkLabeledLeaf a = LabeledBinTree . Fixed $ LeafBT a++mkLabeledFork :: LabeledBinTree a -> a -> LabeledBinTree a -> LabeledBinTree a+mkLabeledFork l a r = LabeledBinTree . Fixed $ ForkBT (_labeledBinTree l) a (_labeledBinTree r)++fmapLabeledBinTree :: (a -> b) -> (LabeledBinTree a -> LabeledBinTree b)+fmapLabeledBinTree f = deconsLabeledBinTree (mkLabeledEmpty) (\a -> mkLabeledLeaf (f a)) (\l a r -> mkLabeledFork (fmapLabeledBinTree f l) (f a) (fmapLabeledBinTree f r))++foldrLabeledBinTree :: (a -> b -> b) -> b -> LabeledBinTree a -> b+foldrLabeledBinTree f z = deconsLabeledBinTree+ z+ (\a -> f a z)+ (\l a r -> foldrLabeledBinTree f (f a (foldrLabeledBinTree f z r)) l)++-- | Replace all forks of empty nodes with leaf nodes.+--+-- We can do this because 'LabeledBinTree' requires leaf and fork node+-- element types to be qual.+simplifyLeavesLabeledBinTree :: LabeledBinTree a -> LabeledBinTree a+simplifyLeavesLabeledBinTree = deconsLabeledBinTree+ mkLabeledEmpty+ (\a -> mkLabeledLeaf a)+ (\l a r -> deconsLabeledBinTree+ (deconsLabeledBinTree+ (mkLabeledLeaf a) -- Replacement: both original left and right trees are empty.+ (\_ra -> mkLabeledFork (simplifyLeavesLabeledBinTree l) a (simplifyLeavesLabeledBinTree r))+ (\_rl _ra _rr -> mkLabeledFork (simplifyLeavesLabeledBinTree l) a (simplifyLeavesLabeledBinTree r))+ r+ )+ (\_la -> mkLabeledFork (simplifyLeavesLabeledBinTree l) a (simplifyLeavesLabeledBinTree r))+ (\_ll _la _lr -> mkLabeledFork (simplifyLeavesLabeledBinTree l) a (simplifyLeavesLabeledBinTree r))+ l+ )++-- | Simplify a LabeledBinTree tree by removing all Leave constructs, replacing+-- leaves with forks of empties. The values of leaves is not lost; they are+-- simply reframed as forks of empties (with the value still present).+simplifyEmptiesLabeledBinTree :: LabeledBinTree a -> LabeledBinTree a+simplifyEmptiesLabeledBinTree = deconsLabeledBinTree+ mkLabeledEmpty+ (\a -> mkLabeledFork mkLabeledEmpty a mkLabeledEmpty)+ (\l a r -> mkLabeledFork l a r )++--- | Normalize a LabeledBinTree with 'simplifyLeavesLabeledBinTree',+--- replacing all forks of empty nodes with leaf nodes.+normalizeLabeledBinTree :: LabeledBinTree a -> LabeledBinTree a+normalizeLabeledBinTree = simplifyLeavesLabeledBinTree++-- | Replace all leaf nodes with a fork node with the given left and right+-- branches.+--+-- Note: normalizing here doesn't change the result if we were to instead check+-- for leaf equivalence manually ourselves.+forkinizeLeaves :: LabeledBinTree a -> LabeledBinTree a -> LabeledBinTree a -> LabeledBinTree a+forkinizeLeaves ll base lr = forkinizeLeavesDirect ll (normalizeLabeledBinTree base) lr++-- | Assuming the base tree is already normalized, replace all leaf nodes in+-- the base tree with a fork node with the given left and right branches.+forkinizeLeavesDirect :: LabeledBinTree a -> LabeledBinTree a -> LabeledBinTree a -> LabeledBinTree a+forkinizeLeavesDirect ll base lr = deconsLabeledBinTree+ mkLabeledEmpty+ (\a -> mkLabeledFork ll a lr)+ (\l a r -> mkLabeledFork (forkinizeLeavesDirect ll l lr) a (forkinizeLeavesDirect ll r lr))+ base++-- | Replace each of the root tree's inner leaf nodes with a fork node whose+-- left branch is the likewise joined outer left tree and whose right branch is+-- the likewise joined outer right tree. This is perhaps like a+-- non-determinism of paths to leaves, perhaps somewhat like list monad+-- non-determinism.+joinLabeledBinTree :: LabeledBinTree (LabeledBinTree a) -> LabeledBinTree a+joinLabeledBinTree = joinDirectLabeledBinTree . normalizeLabeledBinTree . (normalizeLabeledBinTree <$>)++-- | 'joinLabeledBinTree' that assumes the tree and all nested trees have+-- already been normalized.+joinDirectLabeledBinTree :: LabeledBinTree (LabeledBinTree a) -> LabeledBinTree a+joinDirectLabeledBinTree = deconsLabeledBinTree+ mkLabeledEmpty+ (\a -> a)+ (\l a r -> forkinizeLeavesDirect (joinDirectLabeledBinTree l) a (joinDirectLabeledBinTree r))++singletonLabeledBin :: a -> LabeledBinTree a+singletonLabeledBin x = mkLabeledLeaf x++repeatLabeledBinTree :: a -> LabeledBinTree a+repeatLabeledBinTree x = mkLabeledFork (repeatLabeledBinTree x) x (repeatLabeledBinTree x)++pureLabeledBinTreeLossy :: a -> LabeledBinTree a+pureLabeledBinTreeLossy x = repeatLabeledBinTree x++-- | Zip-like lossy Applicative for LabeledBinTree: component-wise.+appLabeledBinTreeLossy :: (LabeledBinTree (a -> b)) -> LabeledBinTree a -> LabeledBinTree b+appLabeledBinTreeLossy mf ma = deconsLabeledBinTree+ (mkLabeledEmpty) -- Discard any remaining elements of ‘ma’.+ (\f ->+ deconsLabeledBinTree+ (mkLabeledEmpty) -- Discard the remaining elements of ‘mf’.+ (\a -> mkLabeledLeaf (f a))+ (\_ a _ -> mkLabeledLeaf (f a)) -- Discard the remaining elements of ‘ma’.+ ma+ )+ (\fl f fr ->+ deconsLabeledBinTree+ (mkLabeledEmpty) -- Discard the remaining elements of ‘mf’.+ (\a -> mkLabeledLeaf (f a)) -- Discard the remaining elements of ‘mf’.+ (\al a ar -> mkLabeledFork+ (appLabeledBinTreeLossy fl al)+ (f a)+ (appLabeledBinTreeLossy fr ar)+ )+ ma+ )+ mf++-- | Add another tree to the right-most leaf of this tree.+--+-- Keep following the right-most node until empty is found, and then replace it+-- with the sub-tree.+labeledBinTreeConcatRightmost :: LabeledBinTree a -> LabeledBinTree a -> LabeledBinTree a+labeledBinTreeConcatRightmost base additional = deconsLabeledBinTree+ additional+ (\a -> mkLabeledFork mkLabeledEmpty a additional)+ (\l a r -> mkLabeledFork l a (labeledBinTreeConcatRightmost r additional))+ base++pureLabeledBinTreeCombinatorial :: a -> LabeledBinTree a+pureLabeledBinTreeCombinatorial x = mkLabeledLeaf x++-- | List-like combinatorial Applicative for LabeledBinTree, with a focus on+-- leaves.+--+-- Note: since ‘m >>= f = join (f <$> m)’+-- and ‘m1 <*> m2 = m1 >>= (\x1 -> m2 >>= (\x2 -> return (x1 x2)))’,+-- then applying a binary tree of integer unaops (e.g. plus 1, times 2, squared+-- plus 3, etc.) to a binary tree of integers, would mean ‘m1 <*> m2’ is:+-- map, for each binop: map, for each integer: singleton of the binop applied+-- to the integer; then join; then join. Before the first join, we have a+-- binary tree of a binary tree of a binary tree: a binary tree of, for each of+-- the unaops, a binary tree of the integers, except the integers are singleton+-- applications of the function to the integer. Joining a binary tree of+-- singletons returns the same binary tree without the singleton wrappers (join+-- composed with pure), so after the first, inner join, we then have: a binary+-- tree of, for each of the unaops, a binary tree of the original integers each+-- applied to that unaop. (e.g. copy the integer tree for each node in the+-- unaop tree, and apply that unop.) We now have a binary tree of binary+-- trees, where each node in the outer tree represents which unary operation /+-- function we applied to the inner list to get the inner tree output for that+-- unaop.+--+-- Finally, given this binary tree of outputs for each unaop, we perform+-- another join: take the root binary tree, representing, say, the mapping of+-- the ‘plus1’ root unaop to the input integers, and then for each leaf, turn+-- it into a fork node where the left gets you a continued pattern for the+-- root's left unaops tree, and likewise for right.+--+-- So the result is you start with the root node function applied to the+-- argument tree, but once you reach any leaf, you can keep descending either+-- left or right (possibly a branch can be Empty), and repeat the original+-- argument/input tree except for different unaops. e.g. go to a leaf for the+-- root unaop, then go left, go to a would-be leaf, then go left again, and+-- then you start the tree for the unaop at the unaop tree's root's left's left+-- node's function.+appLabeledBinTreeCombinatorial :: LabeledBinTree (a -> b) -> LabeledBinTree a -> LabeledBinTree b+appLabeledBinTreeCombinatorial mf ma = joinLabeledBinTree ((\f -> (\a -> f a) <$> ma) <$> mf)++-- | Map leaf nodes, only checking directly for leaves. Normalizing the tree+-- can yield different results. To also map nodes that would be leaves when+-- normalized, use 'mapLeavesLabeledBinTree'.+mapLeavesDirectLabeledBinTree :: (a -> a) -> LabeledBinTree a -> LabeledBinTree a+mapLeavesDirectLabeledBinTree f = deconsLabeledBinTree+ mkLabeledEmpty+ (\a -> mkLabeledLeaf (f a))+ (\l a r -> mkLabeledFork (mapLeavesDirectLabeledBinTree f l) a (mapLeavesDirectLabeledBinTree f r))++-- | 'mapLeavesDirectLabeledBinTree' but treats forks of empty nodes as leaves.+mapLeavesLabeledBinTree :: (a -> a) -> LabeledBinTree a -> LabeledBinTree a+mapLeavesLabeledBinTree f = deconsLabeledBinTree+ mkLabeledEmpty+ (\a -> mkLabeledLeaf (f a))+ --(\l a r -> mkLabeledFork (mapLeavesDirectLabeledBinTree l) a (mapLeavesDirectLabeledBinTree r))+ (\l a r -> deconsLabeledBinTree+ (deconsLabeledBinTree+ ( mkLabeledFork (mapLeavesDirectLabeledBinTree f l) (f a) (mapLeavesDirectLabeledBinTree f r))+ (\_ra -> mkLabeledFork (mapLeavesDirectLabeledBinTree f l) a (mapLeavesDirectLabeledBinTree f r))+ (\_rl _ra _rr -> mkLabeledFork (mapLeavesDirectLabeledBinTree f l) a (mapLeavesDirectLabeledBinTree f r))+ r+ )+ (\_la -> mkLabeledFork (mapLeavesDirectLabeledBinTree f l) a (mapLeavesDirectLabeledBinTree f r))+ (\_ll _la _lr -> mkLabeledFork (mapLeavesDirectLabeledBinTree f l) a (mapLeavesDirectLabeledBinTree f r))+ l+ )++-- | Map each element, skipping leaves.+mapNonleavesLabeledBinTree :: (a -> a) -> LabeledBinTree a -> LabeledBinTree a+mapNonleavesLabeledBinTree f = deconsLabeledBinTree+ mkLabeledEmpty+ mkLabeledLeaf+ (\l a r -> mkLabeledFork (mapNonleavesLabeledBinTree f l) (f a) (mapNonleavesLabeledBinTree f r))++-- | For each element in the (left) tree, replace with the tree produced by+-- applying the function to that element, where each leaf becomes a fork node+-- with its left and right children having likewise binding.+--+-- See 'joinLabeledBinTree' for more information.+bindLabeledBinTree :: LabeledBinTree a -> (a -> LabeledBinTree b) -> LabeledBinTree b+bindLabeledBinTree ma f = joinLabeledBinTree (f <$> ma)++instance Functor LabeledBinTree where+ fmap :: (a -> b) -> (LabeledBinTree a -> LabeledBinTree b)+ fmap = fmapLabeledBinTree++instance Foldable LabeledBinTree where+ foldr :: (a -> b -> b) -> b -> LabeledBinTree a -> b+ foldr = foldrLabeledBinTree++-- | The default Applicative instance of LabeledBinTree is 'appLabeledBinTreeCombinatorial'.+instance Applicative LabeledBinTree where+ pure :: a -> LabeledBinTree a+ pure = pureLabeledBinTreeCombinatorial+ (<*>) :: LabeledBinTree (a -> b) -> LabeledBinTree a -> LabeledBinTree b+ (<*>) = appLabeledBinTreeCombinatorial++-- | The default Monad instance of LabeledBinTree uses 'joinLabeledBinTree'+--+-- If my hand-wavy analysis is correct, joining trees strictly would have+-- exponentially growing space costs, so it may rarely be useful to use this+-- if evaluating the entire tree, rather than only down a specific or select+-- set of paths through the tree.+instance Monad LabeledBinTree where+ return :: a -> LabeledBinTree a+ return = pure+ (>>=) :: LabeledBinTree a -> (a -> LabeledBinTree b) -> LabeledBinTree b+ (>>=) = bindLabeledBinTree++traverseLBT :: Applicative f => (a -> f b) -> LabeledBinTree a -> f (LabeledBinTree b)+traverseLBT f = deconsLabeledBinTree+ ( pure emptyLBT)+ (\a -> pure leafLBT <*> f a)+ (\l a r -> pure forkLBT <*> traverseLBT f l <*> f a <*> traverseLBT f r)++instance Traversable LabeledBinTree where+ traverse :: Applicative f => (a -> f b) -> LabeledBinTree a -> f (LabeledBinTree b)+ traverse = traverseLBT++-- * Sizes++-- | The number of non-empty elements in an lbt.+numElemsLBT :: (Num i) => LabeledBinTree a -> i+numElemsLBT = foldr (\_x acc -> acc + 1) 0++-- | The number of nodes, empty, leaf, or forking.+--+-- Provide the size of empty, leaf, and forking nodes (e.g. 0, 1, 1) and find+-- the sum.+numNodesWeightedDirectLBT :: (Num i) => i -> i -> i -> LabeledBinTree a -> i+numNodesWeightedDirectLBT a b c tree0 = (\f -> f 0 [tree0]) . fix $ \count counter remaining -> case remaining of+ [] -> counter+ (tree:remaining') -> deconsLabeledBinTree+ ( count (counter + a) $ remaining')+ (\_a -> count (counter + b) $ remaining')+ (\l _a r -> count (counter + c) $ l:r:remaining')+ tree++-- | The number of nodes, empty, leaf, or forking.+numNodesDirectLBT :: (Num i) => LabeledBinTree a -> i+numNodesDirectLBT = numNodesWeightedDirectLBT 1 1 1++-- | Count the number of empty nodes.+numEmptyDirectLBT :: (Num i) => LabeledBinTree a -> i+numEmptyDirectLBT = numNodesWeightedDirectLBT 1 0 0++-- | Count the number of leaf nodes.+numLeavesDirectLBT :: (Num i) => LabeledBinTree a -> i+numLeavesDirectLBT = numNodesWeightedDirectLBT 0 1 0++-- | Count the number of fork nodes (branching).+numForkDirectLBT :: (Num i) => LabeledBinTree a -> i+numForkDirectLBT = numNodesWeightedDirectLBT 0 0 1++-- | Count the number of nodes in the tree of any of the 3 kinds.+numAnyDirectLBT :: (Num i) => LabeledBinTree a -> i+numAnyDirectLBT = numNodesWeightedDirectLBT 1 1 1++-- | 'numElemsLBT' specialized to Integer.+numElemsLBTI :: LabeledBinTree a -> Integer+numElemsLBTI = numElemsLBT++-- | 'numNodesWeightedDirectLBT' specialized to Integer.+numNodesWeightedDirectLBTI :: Integer -> Integer -> Integer -> LabeledBinTree a -> Integer+numNodesWeightedDirectLBTI = numNodesWeightedDirectLBT++-- | 'numNodesDirectLBT' specialized to Integer.+numNodesDirectLBTI :: LabeledBinTree a -> Integer+numNodesDirectLBTI = numNodesDirectLBT++-- | 'numEmptyDirectLBT' specialized to Integer.+numEmptyDirectLBTI :: LabeledBinTree a -> Integer+numEmptyDirectLBTI = numEmptyDirectLBT++-- | 'numLeavesDirectLBT' specialized to Integer.+numLeavesDirectLBTI :: LabeledBinTree a -> Integer+numLeavesDirectLBTI = numLeavesDirectLBT++-- | 'numForkDirectLBT' specialized to Integer.+numForkDirectLBTI :: LabeledBinTree a -> Integer+numForkDirectLBTI = numForkDirectLBT++-- | 'numAnyDirectLBT' specialized to Integer.+numAnyDirectLBTI :: LabeledBinTree a -> Integer+numAnyDirectLBTI = numAnyDirectLBT++-- * Constructor aliases++-- | Shorter alias of 'mkLabeledEmpty'.+emptyLBT :: LabeledBinTree a+emptyLBT = mkLabeledEmpty++-- | Shorter alias of 'mkLabeledLeaf'.+leafLBT :: a -> LabeledBinTree a+leafLBT = mkLabeledLeaf++-- | Shorter alias of 'mkLabeledFork'.+forkLBT :: LabeledBinTree a -> a -> LabeledBinTree a -> LabeledBinTree a+forkLBT = mkLabeledFork++-- * Equivalence relations++-- | Equivalence after normalization.+eqLBT :: (Eq a) => LabeledBinTree a -> LabeledBinTree a -> Bool+eqLBT = (==) `on` normalizeLabeledBinTree++-- | Leq after normalization.+leqLBT :: (Ord a) => LabeledBinTree a -> LabeledBinTree a -> Bool+leqLBT = (<=) `on` normalizeLabeledBinTree++-- | fs-style show.+showFs :: (Show a) => LabeledBinTree a -> String+showFs = showFs' show++-- | fs-style show, with a custom Show instance of ‘a’; primitive but still+-- more readable than default Show.+--+-- This implementation is currently relatively inefficient, since there is a+-- lot of unneeded repeated concatenation. Consider DLists if wanting to+-- improve.+showFs' :: (a -> String) -> LabeledBinTree a -> String+showFs' showA tree = (\f -> f "" tree) . fix $ \withPrefix prefix subtree -> deconsLabeledBinTree+ (prefix ++ "×\n")+ (\a -> prefix ++ (showA a) ++ "\n")+ (\l a r -> prefix ++ showA a ++ "\n" ++ withPrefix (prefix ++ "\t.") l ++ withPrefix (prefix ++ "\t·") r)+ subtree
Immutaball/Ball/CLI.hs view
@@ -17,6 +17,8 @@ immutaballOptions, immutaballWithArgs, immutaballHelp,+ immutaballHelp',+ immutaballIntroText, immutaballVersion, immutaballWithCLIConfig, cliIBDirs,@@ -40,6 +42,7 @@ -- external imports import Control.Lens+import Control.Wire.Meta (isFallbackWire) import qualified Data.Text as T import System.FilePath @@ -106,6 +109,21 @@ Option [] ["headless"] (NoArg . b $ cliCfgHeadless .~ True) "", Option [] ["no-headless"] (NoArg . b $ cliCfgHeadless .~ False)+ "",++ Option ['Q'] ["skip-intro-text"] (NoArg . b $ cliCfgSkipIntroText .~ True)+ "",+ Option [] ["no-skip-intro-text"] (NoArg . b $ cliCfgSkipIntroText .~ False)+ "",++ Option [] ["help-detailed"] (NoArg . b $ cliCfgHelpDetailed .~ True)+ "",+ Option [] ["no-help-detailed"] (NoArg . b $ cliCfgHelpDetailed .~ False)+ "",++ Option [] ["request-override-physics"] (ReqArg (\str -> b $ cliCfgOverridePhysics .~ Just (Just str)) "")+ "",+ Option [] ["no-request-override-physics"] (NoArg . b $ cliCfgOverridePhysics .~ Nothing) "" ] where b = CLIConfigBuilder@@ -129,7 +147,10 @@ | otherwise = immutaballWithCLIConfig x'cfg cliCfg immutaballHelp :: String-immutaballHelp = intercalate "\n" $+immutaballHelp = immutaballHelp' False++immutaballHelp' :: Bool -> String+immutaballHelp' detailed = intercalate "\n" $ [ "Usage: immutaball [options…]", "",@@ -143,11 +164,76 @@ "\t--user-config-dir PATH:", "\t\tSet user config directory path.", "\t--headless:",- "\t\tDisable video and audio. Useful for automated testing."+ "\t\tDisable video and audio. Useful for automated testing.",+ "\t-Q, --skip-intro-text:",+ "\t\tSilence the intro text on startup.",+ "\t--help-detailed:",+ "\t\tShow additional usage and exit."+ ] ++ if' (not detailed) []+ [+ "",+ "Additional options:",+ "\t--request-override-physics=default:",+ "\t\tFor debugging or development, select the default physics algorithm.",+ "\t--request-override-physics=stationary:",+ "\t\tFor debugging or development, select the stationary physics algorithm.",+ "\t--request-override-physics=ghostly:",+ "\t\tFor debugging or development, select the ghostly physics algorithm.",+ "\t--request-override-physics=brute-force:",+ "\t\tFor debugging or development, select the brute-force physics algorithm.",+ "\t\tWARNING: does not perform well on large levels and may freeze!",+ "\t--request-override-physics=bsp:",+ "\t\tFor debugging or development, select the bsp physics algorithm." ] +immutaballIntroText :: String+immutaballIntroText = intercalate "\n" $+ [+ "Welcome to Immutaball v" ++ immutaballVersion ++ "!",+ "",+ "WARNING: This prototype version currently only implements",+ " basic functionality; much of the game is yet unimplemented.",+ " A basic GUI, renderer, and physics is currently supported.",+ " Currently, the physics does not fully support moving bodies,",+ " and currently seems to have a few bugs.",+ "",+ "To use Immutaball, you'll need to build Neverball and call Immutaball",+ "with the path to the data dir in the Neverball build:",+ "\tcd ~/git",+ "\tgit clone https://github.com/Neverball/neverball",+ "\tcd ~/git/neverball",+ "\t# git checkout 1.7.0-alpha.3 # Optionally check out the most recent version known to be supported if there are issues.",+ "\tmake -j7",+ "",+ "\tcd ~/git",+ "\tgit clone https://github.com/nibzdil/immutaball",+ "\tcd ~/git/immutaball",+ "\tcabal build --enable-tests",+ "\tcabal run immutaball -- -d ~/git/neverball/data",+ "",+ "Alternatively, if there is a system-installed Neverball package",+ "already built, you can also use its data path instead when passing",+ "‘-d PATH’.",+ ""+ ] +++ (if' (not isFallbackWire)+ [+ "Using external dependency ‘wires’ (not fallback implementation)."+ ]+ [+ "Not using external package ‘wires’ for dependency",+ "(perhaps it failed to build); using fallback implementation",+ "(performance not necessarily optimized)."+ ]+ ) +++ [+ "",+ "Pass ‘--help’ for usage information.",+ "Pass ‘-Q’ to silence this intro text."+ ]+ immutaballVersion :: String-immutaballVersion = "0.1.0.4.1-hackage"+immutaballVersion = "0.1.0.5.1-hackage" -- | Run immutaball. immutaballWithCLIConfig :: StaticConfig -> CLIConfig -> ImmutaballIO@@ -156,13 +242,27 @@ where showHelp :: ImmutaballIO showHelp = mkBIO . PutStrLn immutaballHelp $ mkBIO ExitSuccessBasicIOF+ showHelp' :: Bool -> ImmutaballIO+ showHelp' detailed = mkBIO . PutStrLn (immutaballHelp' detailed) $ mkBIO ExitSuccessBasicIOF showVersion :: ImmutaballIO showVersion = mkBIO . PutStrLn immutaballVersion $ mkBIO ExitSuccessBasicIOF+ showIntroTextThen :: ImmutaballIO -> ImmutaballIO+ showIntroTextThen+ | null immutaballIntroText = id+ | (cliCfg^.cliCfgSkipIntroText) = id+ | otherwise =+ mkBIO . PutStrLn immutaballIntroText+ -- Allow CLI options to update the static config.+ x'cfg' :: StaticConfig+ x'cfg'+ | Just val <- cliCfg^.cliCfgOverridePhysics = x'cfg & x'cfgRequestAlternativePhysics .~ val+ | otherwise = x'cfg result :: ImmutaballIO result- | cliCfg^.cliCfgHelp = showHelp- | cliCfg^.cliCfgVersion = showVersion- | otherwise = immutaballWithCLIConfig' x'cfg cliCfg+ | cliCfg^.cliCfgHelpDetailed = showHelp' True+ | cliCfg^.cliCfgHelp = showHelp+ | cliCfg^.cliCfgVersion = showVersion+ | otherwise = showIntroTextThen $ immutaballWithCLIConfig' x'cfg' cliCfg cliIBDirs :: CLIConfig -> IBDirs -> IBDirs cliIBDirs cliCfg defaultIBDirs = IBDirs {
Immutaball/Ball/CLI/Config.hs view
@@ -11,6 +11,7 @@ ( CLIConfig(..), cliCfgHelp, cliCfgVersion, cliCfgStaticDataDir, cliCfgUserDataDir, cliCfgUserConfigDir, cliCfgHeadless,+ cliCfgSkipIntroText, cliCfgHelpDetailed, cliCfgOverridePhysics, defaultCLIConfig, CLIConfigBuilder(..), modifyCLIConfig, buildCLIConfig@@ -22,24 +23,30 @@ import Control.Lens data CLIConfig = CLIConfig {- _cliCfgHelp :: Bool,- _cliCfgVersion :: Bool,- _cliCfgStaticDataDir :: Maybe FilePath,- _cliCfgUserDataDir :: Maybe FilePath,- _cliCfgUserConfigDir :: Maybe FilePath,- _cliCfgHeadless :: Bool+ _cliCfgHelp :: Bool,+ _cliCfgVersion :: Bool,+ _cliCfgStaticDataDir :: Maybe FilePath,+ _cliCfgUserDataDir :: Maybe FilePath,+ _cliCfgUserConfigDir :: Maybe FilePath,+ _cliCfgHeadless :: Bool,+ _cliCfgSkipIntroText :: Bool,+ _cliCfgHelpDetailed :: Bool,+ _cliCfgOverridePhysics :: Maybe (Maybe String) } deriving (Eq, Ord) makeLenses ''CLIConfig defaultCLIConfig :: CLIConfig defaultCLIConfig = CLIConfig {- _cliCfgHelp = False,- _cliCfgVersion = False,- _cliCfgStaticDataDir = Nothing,- _cliCfgUserDataDir = Nothing,- _cliCfgUserConfigDir = Nothing,- _cliCfgHeadless = False+ _cliCfgHelp = False,+ _cliCfgVersion = False,+ _cliCfgStaticDataDir = Nothing,+ _cliCfgUserDataDir = Nothing,+ _cliCfgUserConfigDir = Nothing,+ _cliCfgHeadless = False,+ _cliCfgSkipIntroText = False,+ _cliCfgHelpDetailed = False,+ _cliCfgOverridePhysics = Nothing } newtype CLIConfigBuilder = CLIConfigBuilder {_modifyCLIConfig :: CLIConfig -> CLIConfig}
Immutaball/Ball/Game.hs view
@@ -297,6 +297,7 @@ initialGameState cxt neverballrc hasLevelBeenCompleted mlevelSet solPath sol = fix $ \gs -> let solAnalysis = mkSolAnalysis cxt (gs^.gsSol) in par (M.size $ solAnalysis^.saPhysicsAnalysis.spaLumpPlanes) .+ par (solAnalysis^.saPhysicsAnalysis.spaBSPNumPartitions) . par solAnalysis $ GameState { _gsGameMode = Intermission,
+ Immutaball/Ball/Level.hs view
@@ -0,0 +1,17 @@+{-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .+-- Enable warnings:+{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}++-- Level.hs.++{-# LANGUAGE Haskell2010 #-}++module Immutaball.Ball.Level+ (+ module Immutaball.Ball.Level.Render+ ) where++import Prelude ()+--import Immutaball.Prelude++import Immutaball.Ball.Level.Render
+ Immutaball/Ball/Level/Render.hs view
@@ -0,0 +1,265 @@+{-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .+-- Enable warnings:+{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}++-- Level/Render.hs.++{-# LANGUAGE Haskell2010 #-}+{-# LANGUAGE Arrows, ScopedTypeVariables #-}++-- | Level rendering.+module Immutaball.Ball.Level.Render+ (+ renderLevel,+ renderSetupNewLevel,+ renderScene,+ renderGeomPass+ ) where++import Prelude ()+import Immutaball.Prelude++import Control.Arrow+import Control.Monad+import Data.Int+import Data.Ix+import Data.Maybe++import Control.Lens+import Data.Array.IArray+import qualified Data.Map.Lazy as M+import Graphics.GL.Compatibility45+--import Graphics.GL.Core45+import Graphics.GL.Types++import Immutaball.Ball.Game+import Immutaball.Ball.State.Game -- rendererTransformationMatrix, getPathTranslation+import Immutaball.Share.Config+import Immutaball.Share.Context+import Immutaball.Share.ImmutaballIO.GLIO+import Immutaball.Share.Level.Analysis+import Immutaball.Share.Level.Base+import Immutaball.Share.Math+import Immutaball.Share.SDLManager+import Immutaball.Share.State+import Immutaball.Share.State.Context+import Immutaball.Share.Utils+import Immutaball.Share.Video+import Immutaball.Share.Wire++renderLevel :: Wire ImmutaballM ((MView, SolWithAnalysis, GameState), IBStateContext) IBStateContext+renderLevel = proc ((camera_, swa, gs), cxtn) -> do+ -- Set up.+ let levelPath = swa^.swaMeta.smPath+ (mlastLevelPath, cxtnp1) <- setCurrentlyLoadedSOL -< (levelPath, cxtn)+ let newLevel = not $ Just levelPath == mlastLevelPath+ cxtnp2 <- returnA ||| renderSetupNewLevel -< if' (not newLevel) (Left cxtnp1) (Right (swa, cxtnp1))++ -- Render the scene.+ cxtnp3 <- renderScene -< ((camera_, swa, gs), cxtnp2)++ -- Return the state context.++ let cxt = cxtnp3+ returnA -< cxt++renderSetupNewLevel :: Wire ImmutaballM (SolWithAnalysis, IBStateContext) IBStateContext+renderSetupNewLevel = proc (swa, cxtn) -> do+ let sra = swa^.swaSa.saRenderAnalysis++ -- Upload SSBOs.+ cxtnp1 <- setSSBO -< ((shaderSSBOVertexDataLocation, sra^.sraVertexDataGPU), cxtn)+ cxtnp2 <- setSSBO -< ((shaderSSBOGeomDataLocation, sra^.sraGeomDataGPU), cxtnp1)+ cxtnp3 <- setSSBO -< ((shaderSSBOLumpDataLocation, sra^.sraLumpDataGPU), cxtnp2)+ cxtnp4 <- setSSBO -< ((shaderSSBOPathDoublesDataLocation, sra^.sraPathDoublesDataGPU), cxtnp3)+ cxtnp5 <- setSSBO -< ((shaderSSBOPathInt32sDataLocation, sra^.sraPathInt32sDataGPU), cxtnp4)+ cxtnp6 <- setSSBO -< ((shaderSSBOBodyDataLocation, sra^.sraBodyDataGPU), cxtnp5)+ cxtnp7 <- setSSBO -< ((shaderSSBOGcDataLocation, sra^.sraGcArrayGPU), cxtnp6)+ cxtnp8 <- setSSBO -< ((shaderSSBOAllGeomPassMvDataLocation, sra^.sraAllGeomPassMvGPU), cxtnp7)+ cxtnp9 <- setSSBO -< ((shaderSSBOAllGeomPassTexturesDataLocation, sra^.sraAllGeomPassTexturesGPU), cxtnp8)+ cxtnp10 <- setSSBO -< ((shaderSSBOAllGeomPassGisDataLocation, sra^.sraAllGeomPassGisGPU), cxtnp9)+ cxtnp11 <- setSSBO -< ((shaderSSBOGeomPassMvRangesDataLocation, sra^.sraGeomPassMvRangesGPU), cxtnp10)+ cxtnp12 <- setSSBO -< ((shaderSSBOGeomPassTexturesRangesDataLocation, sra^.sraGeomPassTexturesRangesGPU), cxtnp11)+ cxtnp13 <- setSSBO -< ((shaderSSBOGeomPassGisRangesDataLocation, sra^.sraGeomPassGisRangesGPU), cxtnp12)+ cxtnp14 <- setSSBO -< ((shaderSSBOGeomPassBisDataLocation, sra^.sraGeomPassBisGPU), cxtnp13)+ cxtnp15 <- setSSBO -< ((shaderSSBOTexcoordsDoubleDataLocation, sra^.sraTexcoordsDoubleDataGPU), cxtnp14)++ -- Upload elems vao and buf.+ cxtnp16 <- setElemVaoVboEbo -< (sra^.sraGcArrayGPU, True, cxtnp15)++ -- Pre-initialize the transformation matrix with the identity.+ cxtnp17 <- setTransformation -< (identity4, cxtnp16)++ -- Approximate disabling OpenGL clipping by depth (by our transformed y coordinates, which transform into OpenGL z depth coordinates).+ -- Also other miscellaneous OpenGL setup.+ let sdlGL1'_ = sdlGL1 (cxtnp17^.ibContext.ibSDLManagerHandle)+ let sdlGL1' = liftIBIO . sdlGL1'_+ () <- monadic -< sdlGL1' $ do+ -- TODO FIXME: this doesn't seem to actually stop clipping for depth+ -- values outside a certain range.+ --+ -- To investigate, disable rescaleDepth, try retour de force level 2,+ -- enable free camera, and rotate the camera until you can see the+ -- level. Move toward that location, and then you can see that the+ -- distance from the camera needs to be only within a certain range.+ --+ -- So instead we'll just use rescaleDepth.+ GLDepthRange (cxtnp17^.ibContext.ibStaticConfig.x'glNearVal) (cxtnp17^.ibContext.ibStaticConfig.x'glFarVal) ()+ -- Other miscellaneous OpenGL setup.+ () <- monadic -< sdlGL1' $ do+ --GLPlaceholder GL_VAL ()+ pure ()++ -- Return the state context.++ let cxt = cxtnp17++ returnA -< cxt++-- | After setup, render the scene.+renderScene :: Wire ImmutaballM ((MView, SolWithAnalysis, GameState), IBStateContext) IBStateContext+renderScene = proc ((camera_, swa, gs), cxtn) -> do+ -- Set up the transformation matrix.+ --let mat = rendererTransformationMatrix camera_+ mat <- arr $ uncurry3 rendererTransformationMatrix -< (cxtn, gs, camera_)+ cxtnp1 <- setTransformation -< (mat, cxtn)++ -- Render the scene.+ let+ sol :: Sol+ sol = swa^.swaSol++ sa :: SolAnalysis+ sa = swa^.swaSa++ sra :: SolRenderAnalysis+ sra = sa^.saRenderAnalysis+ let _unused = (sol)++ let+ ourFlatten :: (Int32, (MView, SolWithAnalysis, GameState, Bool, GeomPass)) -> (Int32, MView, SolWithAnalysis, GameState, Bool, GeomPass)+ ourFlatten (a, (b, c, d, e, f)) = (a, b, c, d, e, f)+ let geomPasses = map ourFlatten . zip [0..] $ map (\gp -> (camera_, swa, gs, False, gp)) (sra^.sraOpaqueGeoms) ++ map (\gp -> (camera_, swa, gs, True, gp)) (sra^.sraTransparentGeoms)+ cxtnp2 <- foldlA renderGeomPass -< (cxtnp1, geomPasses)++ -- Return the state context.+ let cxt = cxtnp2++ returnA -< cxt++-- | Render a partition of the level geometry, so that we can handle processing up to 16 textures at a time.+renderGeomPass :: Wire ImmutaballM (IBStateContext, (Int32, MView, SolWithAnalysis, GameState, Bool, GeomPass)) IBStateContext+renderGeomPass = proc (cxtn, (geomPassIdx, camera_, swa, gs, isAlpha, gp)) -> do+ -- Setup.+ let sdlGL1'_ = sdlGL1 (cxtn^.ibContext.ibSDLManagerHandle)+ let sdlGL1' = liftIBIO . sdlGL1'_++ -- Render the geom pass.++ -- First, if the body is on a path, update the transformation matrix to+ -- reflect the translation on the path. TODO: currently we only support+ -- translations on paths here.+ let bi = gp^.gpBi+ let body = (swa^.swaSol.solBv) ! bi+ let (moriginPath :: Maybe Int32) = do+ let originPath = body^.bodyP0+ guard . inRange (bounds $ swa^.swaSol.solPv) $ originPath+ return $ originPath+ let (mpathTransformation :: Maybe (Mat4 Double)) = do+ originNode <- moriginPath+ let bodyTranslation = getPathTranslation (swa^.swaSol) (swa^.swaSa.saOtherAnalysis) gs originNode 0+ let transformation = translate3 bodyTranslation+ return transformation+ mat <- arr $ uncurry3 rendererTransformationMatrix -< (cxtn, gs, camera_)+ let (mnetBodyTransformation :: Maybe (Mat4 Double)) = do+ pathTransformation <- mpathTransformation+ return $ mat <> pathTransformation+ cxtnp1 <- returnA ||| setTransformation -< deconsMaybe (Left cxtn) (\pathTransformation -> Right (pathTransformation, cxtn)) $ mnetBodyTransformation++ -- Render all 16 mtrls in the geom pass.+ (mtrlsMeta :: [((WidthHeightI, GLuint), MtrlMeta)], cxtnp2) <-+ foldrA cachingRenderMtrlAccum -< (([], cxtnp1), map (\mi -> (swa^.swaSol, mi)) (elems (gp^.gpMv)))+ let (mtrlsGlTextures :: [GLuint]) = map (fst >>> snd) mtrlsMeta+ let (assignTextures :: GLIOF ()) = do+ forM_ (zip [0..] mtrlsGlTextures) $ \(idx, glTexture) -> do+ case flip M.lookup numToGL_TEXTUREi idx of+ Nothing -> return ()+ Just gl_TEXTUREi -> do+ -- Apparently actually we set GL_TEXTUREi for the texture()+ -- GLSL call, not the texture name. So set the i used in+ -- GL_TEXTUREi as the value, not the texture identifier.+ --GLUniform1i (fromIntegral idx) (fromIntegral texture) ()+ GLUniform1i (fromIntegral idx) (fromIntegral idx) ()++ --GLActiveTexture GL_TEXTUREi ()+ GLActiveTexture gl_TEXTUREi ()+ GLBindTexture GL_TEXTURE_2D glTexture ()++ -- Render all geometry in this geom pass.+ (melemVaoVboEbo, cxtnp3) <- getElemVaoVboEbo -< cxtnp2+ let elemVaoVboEbo = fromMaybe (error "Internal error: renderGeomPass expected elem vao and buf to be present, but it's missing!") melemVaoVboEbo+ let (elemVao, _elemVbo, _elemEbo) = elemVaoVboEbo+ let (renderGeomPassScene :: GLIOF ()) = do+ -- Tell the shaders to enable the scene data.+ GLUniform1i shaderEnableSceneDataLocation trueAsIntegral ()+ -- Tell the shaders we are not drawing the ball right now.+ GLUniform1i shaderEnableBallDataLocation falseAsIntegral ()++ -- Tell the shaders what geom pass to use.+ GLUniform1i (shaderSceneGeomPassIdxLocation) (fromIntegral geomPassIdx) ()++ -- Tell the shaders to render this pass's geometries. It will handle the rest; it already has the geom pass data we uploaded upon setup.+ GLBindVertexArray elemVao ()++ -- Use the vao to tell the shader to draw the geometry.+ let numGpGis = rangeSize . bounds $ gp^.gpGis+ GLDrawArrays GL_TRIANGLES 0 (fromIntegral (3 * numGpGis)) ()++ -- Unbind the VAO.+ GLBindVertexArray 0 ()++ let (alphaSetup :: GLIOF ()) = do+ if isAlpha+ then do+ GLEnable GL_BLEND ()+ GLBlendEquationSeparate GL_FUNC_ADD GL_FUNC_ADD ()+ GLBlendFuncSeparate GL_SRC_ALPHA GL_ONE_MINUS_SRC_ALPHA GL_ONE GL_ZERO ()++ GLEnable GL_DEPTH_TEST ()+ GLDepthMask GL_FALSE ()+ else do+ GLDisable GL_BLEND ()++ GLEnable GL_DEPTH_TEST ()+ GLDepthMask GL_TRUE ()+ let (alphaFinish :: GLIOF ()) = do+ if isAlpha+ then do+ GLDisable GL_BLEND ()++ GLDepthMask GL_TRUE ()+ else do+ GLDisable GL_BLEND ()++ GLDepthMask GL_TRUE ()++ -- Now aggregate the rendering to render the geom pass.+ let (renderGeomPassGL :: GLIOF ()) = do+ alphaSetup+ assignTextures+ renderGeomPassScene+ alphaFinish+ () <- monadic -< sdlGL1' renderGeomPassGL++ -- Return the state context.+ let cxt = cxtnp3+ returnA -< cxt++ where+ -- | Add 2 variants: lookup sol mtrl for path, and also accumulate results in a list.+ cachingRenderMtrlAccum :: Wire ImmutaballM ((LevelIB, Int32), ([((WidthHeightI, GLuint), MtrlMeta)], IBStateContext)) ([((WidthHeightI, GLuint), MtrlMeta)], IBStateContext)+ cachingRenderMtrlAccum = proc ((sol, mi), (accum_, cxtn)) -> do+ let mtrl = (sol^.solMv) ! mi+ let mtrlPath = mtrl^.mtrlF+ (glTextureMeta, cxtnp1) <- cachingRenderMtrl -< (mtrlPath, cxtn)+ returnA -< (glTextureMeta:accum_, cxtnp1)
Immutaball/Ball/State/Game.hs view
@@ -5,913 +5,1958 @@ -- Play.hs. {-# LANGUAGE Haskell2010 #-}-{-# LANGUAGE TemplateHaskell, Arrows, ScopedTypeVariables #-}---- | Game state and immutaball state interface.-module Immutaball.Ball.State.Game- (- GameRequest(..), giRequest, giGameState, giIBStateContext, grRequest,- GameResponse(..), goGameEvents, goGameState, goIBStateContext, grGameEvents,- GameEvent(..), AsGameEvent(..),- stepGame,- renderBall,- renderBallSetup,- stepGameInput,- stepGameInputMovement,- stepGameInputEvents,- stepGameClock,- stepGameClockDebugFreeCamera,- stepGameBallPhysics,- physicsBallAdvance,- physicsBallAdvanceStationary,- physicsBallAdvanceGhostly,- physicsBallAdvanceBruteForce,- physicsBallAdvanceBruteForceCompute-- -- * Local utils.- ) where--import Prelude ()-import Immutaball.Prelude--import Control.Monad-import Data.Int-import Data.Foldable-import Data.List-import qualified Data.Map.Lazy as M-import Data.Maybe--import Control.Arrow-import Control.Lens-import Data.Array.IArray-import Graphics.GL.Compatibility45---import Graphics.GL.Core45-import Graphics.GL.Types-import qualified SDL.Raw.Enum as Raw--import Immutaball.Ball.Game-import Immutaball.Share.Config-import Immutaball.Share.Context-import Immutaball.Share.ImmutaballIO.GLIO-import Immutaball.Share.Level-import Immutaball.Share.Level.Analysis-import Immutaball.Share.Math-import Immutaball.Share.SDLManager-import Immutaball.Share.State-import Immutaball.Share.State.Context-import Immutaball.Share.Utils-import Immutaball.Share.Video-import Immutaball.Share.Wire---- TODO: implement.--data GameRequest = GameRequest {- _giRequest :: Request,- _giGameState :: GameState,- _giIBStateContext :: IBStateContext-}-makeLenses ''GameRequest-grRequest :: Lens' GameRequest Request-grRequest = giRequest--data GameResponse = GameResponse {- _goGameEvents :: [GameEvent],- _goGameState :: GameState,- _goIBStateContext :: IBStateContext-}---makeLenses ''GameResponse--data GameEvent =- NewGameMode GameMode- -- | PlaceholderEvent-makeLenses ''GameResponse-makeClassyPrisms ''GameEvent--grGameEvents :: Lens' GameResponse [GameEvent]-grGameEvents = goGameEvents---- | Step the game.--- TODO: finish implementing.-stepGame :: Wire ImmutaballM GameRequest GameResponse-stepGame = proc gr -> do- let request = gr^.giRequest- let cxtn = gr^.giIBStateContext-- -- Get starting game state.- let (gsn :: GameState) = (gr^.giGameState)-- -- Handle input (in request). (With many possibly input requests, stepGameInput itself handles delegation of this.)- (gsnp1, cxtnp1) <- stepGameInput -< (gsn, request, cxtn)-- -- Step the game on clock request. (Only one request is clock; handle it here it.)- mdt <- returnA -< const Nothing ||| Just $ matching _Clock request- (gsnp2, cxtnp2) <- returnA ||| stepGameClock -< maybe (Left (gsnp1, cxtnp1)) (\dt -> Right (gsn, dt, cxtnp1)) $ mdt-- -- Identify new game state.- let (gs :: GameState) = gsnp2- let (cxt :: IBStateContext) = cxtnp2-- -- Return results.- returnA -< GameResponse {- _goGameEvents = [], -- TODO: detect new game mode.- _goGameState = gs,- _goIBStateContext = cxt- }--renderBall :: Wire ImmutaballM (GameState, IBStateContext) IBStateContext-renderBall = proc (gs, cxtn) -> do- hasInit <- delay False -< returnA True- cxtnp1 <- arr snd ||| renderBallSetup -< if' hasInit Left Right (gs, cxtn)-- let sdlGL1' = liftIBIO . sdlGL1 (cxtnp1^.ibContext.ibSDLManagerHandle)-- -- Render the ball.- (mballElemVaoVboEbo, cxtnp2) <- getBallElemVaoVboEbo -< cxtnp1- let ballElemVaoVboEbo = fromMaybe (error "Internal error: renderBall expected elem vao and buf to be present, but it's missing!") mballElemVaoVboEbo- let (ballElemVao, _ballElemVbo, _ballElemEbo) = ballElemVaoVboEbo-- -- TODO: set the transformation matrix explicitly. For now we take- -- advantage of the fact that the transformation matrix hasn't been updated- -- since the scene was drawn (before the ball was), so just conveniently- -- implicitly re-use the transformation matrix in the meantime.-- let (renderBallDirect :: GLIOF ()) = do- -- Tell the shaders to disable the scene data.- GLUniform1i shaderEnableSceneDataLocation falseAsIntegral ()- -- Tell the shaders we are drawing the ball right now.- GLUniform1i shaderEnableBallDataLocation trueAsIntegral ()-- -- Tell the shaders the ball radius.- let (ballRadiusf :: GLfloat) = realToFrac $ gs^.gsBallRadius- GLUniform1f shaderBallRadiusLocation ballRadiusf ()- -- Tell the shaders the ball num triangles.- let (ballNumTriangles :: Integer) = cxtnp2^.ibContext.ibStaticConfig.x'cfgBallTriangles- GLUniform1i shaderBallNumTrianglesLocation (fromIntegral ballNumTriangles) ()- -- Tell the shaders the ball position.- let (ballPosX :: GLfloat, ballPosY :: GLfloat, ballPosZ :: GLfloat) = (realToFrac $ gs^.gsBallPos.x3, realToFrac $ gs^.gsBallPos.y3, realToFrac $ gs^.gsBallPos.z3)- GLUniform3f shaderBallPosLocation ballPosX ballPosY ballPosZ ()- -- Tell the shaders the ball rotation.- let (ballRotX :: GLfloat, ballRotY :: GLfloat, ballRotZ :: GLfloat) = (realToFrac $ gs^.gsBallRot.x3, realToFrac $ gs^.gsBallRot.y3, realToFrac $ gs^.gsBallRot.z3)- GLUniform3f shaderBallRotLocation ballRotX ballRotY ballRotZ ()-- -- Tell the shaders to render the ball's triangles' vertices. The- -- shader for now will just directly draw a basic ball.- -- (TODO: support more than just basic ball.)- GLBindVertexArray ballElemVao ()-- -- Use the vao to tell the shader to draw the geometry.- --let (ballNumTriangles :: Integer) = cxtnp2^.ibContext.ibStaticConfig.x'cfgBallTriangles- GLDrawArrays GL_TRIANGLES 0 (fromIntegral (3 * ballNumTriangles)) ()-- -- Unbind the VAO.- GLBindVertexArray 0 ()- -- We're not using textures for the basic ball; the shader can calculate the color.- let (assignTextures :: GLIOF ()) = return ()- -- Enable transparency for the ball.- let (isAlpha :: Bool) = True- let (alphaSetup :: GLIOF ()) = do- if isAlpha- then do- GLEnable GL_BLEND ()- GLBlendEquationSeparate GL_FUNC_ADD GL_FUNC_ADD ()- GLBlendFuncSeparate GL_SRC_ALPHA GL_ONE_MINUS_SRC_ALPHA GL_ONE GL_ZERO ()-- GLEnable GL_DEPTH_TEST ()- GLDepthMask GL_FALSE ()- else do- GLDisable GL_BLEND ()-- GLEnable GL_DEPTH_TEST ()- GLDepthMask GL_TRUE ()- let (alphaFinish :: GLIOF ()) = do- if isAlpha- then do- GLDisable GL_BLEND ()-- GLDepthMask GL_TRUE ()- else do- GLDisable GL_BLEND ()-- GLDepthMask GL_TRUE ()-- let (doRenderBall :: GLIOF ()) = do- alphaSetup- assignTextures- renderBallDirect- alphaFinish- () <- monadic -< sdlGL1' doRenderBall-- let cxt = cxtnp2- returnA -< cxt--renderBallSetup :: Wire ImmutaballM (GameState, IBStateContext) IBStateContext-renderBallSetup = proc (_gs0, cxtn) -> do- -- A new game state hasn't had its ball render setup performed yet.- -- Perform setup.-- -- First, set up ball elem vao and vbo. See if a previous setup has- -- already done it. Race conditions are okay here, since setting the ball- -- elem vao would discard an old elem vao in a race properly.- -- (Alternatively, we could also always set it up here since it would only- -- happen on a new game state, but checking probably saves a bit of- -- performance for uploading the VAO.)- (mballElemVaoVboEbo, cxtnp1) <- getBallElemVaoVboEbo -< cxtn-- -- Set up the data, ballElemVaoVboEboDataGPU.- let (ballNumTriangles :: Integer) = cxtnp1^.ibContext.ibStaticConfig.x'cfgBallTriangles- let (ballElemVaoVboEboData :: Array Int32 Int32) = genArray (0, 3 * fromIntegral ballNumTriangles - 1) $ \idx -> idx- let (ballElemVaoVboEboDataGPU :: GLData) = gpuEncodeArray ballElemVaoVboEboData-- -- Upload ball elems vao and buf. (Just returnA the cxtnp1 if there's already a ball VAO.)- cxtnp2 <- arr (^._3) ||| setBallElemVaoVboEbo -< if' (isJust mballElemVaoVboEbo) Left Right (ballElemVaoVboEboDataGPU, True, cxtnp1)-- let cxt = cxtnp2- returnA -< cxt---- | Handle input.-stepGameInput :: Wire ImmutaballM (GameState, Request, IBStateContext) (GameState, IBStateContext)-stepGameInput = proc (gsn, request, cxtn) -> do- -- Handle forward, right, etc., movement.- (gsnp1, cxtnp1) <- stepGameInputMovement -< (gsn, request, cxtn)-- -- Handle clicking to start playing, etc.- (gsnp2, cxtnp2) <- stepGameInputEvents -< (gsnp1, request, cxtnp1)-- -- Identify output.- let cxt = cxtnp2- let gs = gsnp2-- -- Return.- returnA -< (gs, cxt)--stepGameInputMovement :: Wire ImmutaballM (GameState, Request, IBStateContext) (GameState, IBStateContext)-stepGameInputMovement = proc (gsn, request, cxtn) -> do- -- Movement.- let update = case request of- (Keybd char down) ->- if' (char == cxtn^.ibNeverballrc.keyForward) (gsInputState.ginsForwardOn .~ down) .- if' (char == cxtn^.ibNeverballrc.keyBackward) (gsInputState.ginsBackwardOn .~ down) .- if' (char == cxtn^.ibNeverballrc.keyLeft) (gsInputState.ginsLeftOn .~ down) .- if' (char == cxtn^.ibNeverballrc.keyRight) (gsInputState.ginsRightOn .~ down) .- if' (char == cxtn^.ibContext.ibStaticConfig.x'cfgVertUpKey) (gsInputState.ginsVertUpOn .~ down) .- if' (char == cxtn^.ibContext.ibStaticConfig.x'cfgVertDownKey) (gsInputState.ginsVertDownOn .~ down) $- id- (Click _button@Raw.SDL_BUTTON_LEFT down) -> (gsInputState.ginsMouseLeftOn .~ down)- (Click _button@Raw.SDL_BUTTON_RIGHT down) -> (gsInputState.ginsMouseRightOn .~ down)- _ -> id-- let gsnp1 = gsn & update-- -- Toggle debug camera view.- let freeCamToggleUpdate = if' (not $ cxtn^.ibContext.ibStaticConfig.x'cfgDebugFreeCamera) id $- case request of- (Keybd char True) ->- if' (char == cxtn^.ibContext.ibStaticConfig.x'cfgFreeCameraToggleKey) (gsDebugState.gdsCameraDebugOn %~ not) $- id- _ -> id-- let gsnp2 = gsnp1 & freeCamToggleUpdate-- -- Free camera aiming.- -- mouse sense gives int in pixels for a full circle rotation, except we also scale rotation amount by curMouseSpeed (the 2 speeds are multiplied (although neverballrc is reciprocal)).- let (curMouseSpeed :: Double) = 4.0- let (curMouseSense :: Int) = cxtn^.ibNeverballrc.mouseSense- let updateFreeAim = if' (not $ gsnp2^.gsDebugState.gdsCameraDebugOn) id $ case request of- (Point _x _y dx' dy') ->- -- SDL for me reported inverted dy (i.e. SDL's 0,0 at the top-left corner); so invert dy.- let dx = dx' in- let dy = -dy' in-- let (dxd :: Double) = fromIntegral dx in- let (dyd :: Double) = fromIntegral dy in-- let (drawCirclesX :: Double) = dxd / fromIntegral curMouseSense in- let (drawCirclesY :: Double) = dyd / fromIntegral curMouseSense in-- let (drawRadiansX :: Double) = tau * drawCirclesX in- let (drawRadiansY :: Double) = tau * drawCirclesY in-- let (dradiansX :: Double) = curMouseSpeed * drawRadiansX in- let (dradiansY :: Double) = curMouseSpeed * drawRadiansY in-- (gsDebugState.gdsCameraAimRightRadians %~ (+ dradiansX)) .- (gsDebugState.gdsCameraAimUpRadians %~ (+ dradiansY)) .- id- _ -> id-- -- Cap: let net view be capped later in 'mkGameStateAnalysis', since there might already be a vertical aim to target.- -- But also cap here at twice the radius to allow full movement.- let updateCapFreeAimUp = gsDebugState.gdsCameraAimUpRadians %~ (min (2 * tau/2) . max (-2 * tau/2))-- let gsnp3 = gsnp2 & updateCapFreeAimUp . updateFreeAim-- -- Tilt the world if playing.- -- The config for curMouseSense is interpreted to mean the number of pixels- -- needed to traverse the entire tilt range (2 * x'cfgMaxTilt).- let (mouseTiltModifier :: Double) = 1.0 -- Don't change mouse sensitivity for tilt.- let (mouseSensitivity :: Int) = cxtn^.ibNeverballrc.mouseSense- let (maxTilt :: Double) = cxtn^.ibContext.ibStaticConfig.x'cfgMaxTilt- let updateTiltBase = if' (not . isPlaying $ gsnp1^.gsGameMode) id $ case request of- (Point _x _y dx' dy') ->- -- SDL for me reported inverted dy (i.e. SDL's 0,0 at the top-left corner); so invert dy.- let dx = dx' in- let dy = -dy' in-- let (dxd :: Double) = fromIntegral dx in- let (dyd :: Double) = fromIntegral dy in-- let (dTiltRangesX :: Double) = dxd / fromIntegral mouseSensitivity in- let (dTiltRangesY :: Double) = dyd / fromIntegral mouseSensitivity in-- let (dRawRadiansX :: Double) = tau * dTiltRangesX in- let (dRawRadiansY :: Double) = tau * dTiltRangesY in-- let (dRadiansX :: Double) = mouseTiltModifier * dRawRadiansX in- let (dRadiansY :: Double) = mouseTiltModifier * dRawRadiansY in-- (gsGravityState.gravsTiltRightRadians %~ (+ dRadiansX)) .- (gsGravityState.gravsTiltForwardRadians %~ (+ dRadiansY)) .- id- _ -> id- let updateTiltCap =- (gsGravityState.gravsTiltRightRadians %~ (min maxTilt . max (-maxTilt))) .- (gsGravityState.gravsTiltForwardRadians %~ (min maxTilt . max (-maxTilt))) .- id- let updateTilt = updateTiltCap <<< updateTiltBase-- let gsnp4 = gsnp3 & updateTilt-- -- Identify output.- let gs = gsnp4- let cxt = cxtn-- -- Return.- returnA -< (gs, cxt)---- | Handle clicking to start playing, etc.--- TODO: generate event for new game mode.-stepGameInputEvents :: Wire ImmutaballM (GameState, Request, IBStateContext) (GameState, IBStateContext)-stepGameInputEvents = proc (gsn, request, cxtn) -> do- -- Start playing?- let update = if' (gsn^.gsGameMode /= Intermission) id $ case request of- (Click _button@Raw.SDL_BUTTON_LEFT _down@True) -> (gsGameMode .~ Playing)- _ -> id-- let gsnp1 = gsn & update-- -- Identify output.- let gs = gsnp1- let cxt = cxtn-- -- Return.- returnA -< (gs, cxt)---- | Step a frame of the game state on clock.-stepGameClock :: Wire ImmutaballM (GameState, Double, IBStateContext) (GameState, IBStateContext)-stepGameClock = proc (gsn, dt, cxtn) -> do- let gsa = mkGameStateAnalysis cxtn gsn-- -- Step debug camera.- (gsnp1, cxtnp1) <- returnA ||| stepGameClockDebugFreeCamera -<- if' (not $ gsn^.gsDebugState.gdsCameraDebugOn)- (Left (gsn, cxtn))- (Right (gsn, dt, cxtn))-- -- Advance time elapsed if in play state.- let cxtnp2 = cxtnp1- let gsnp2 = gsnp1 &- if' (not . isPlaying $ gsnp1^.gsGameMode)- (id)- (gsTimeElapsed %~ (+ dt))-- -- Respond to left and right mouse down by moving the camera.- rec- lastNetMouseRight <- delay 0 -< netMouseRight- netMouseRight <- returnA -< gsa^.gsaNetMouseRight-- lastIsPlayingState <- delay False -< isPlayingState- isPlayingState <- returnA -< isPlaying $ gsnp2^.gsGameMode-- foundChange <- hold False -< if' ((lastIsPlayingState == isPlayingState) && (lastNetMouseRight /= netMouseRight)) (Just True) Nothing-- let cameraAngleSpeed = 1.0 -- radians per second -- TODO: use neverballrc rotate_slow- let updateCameraAngle = if' (not foundChange || not isPlayingState) id $- (gsCameraAngle %~ (+ (-fromIntegral netMouseRight) * cameraAngleSpeed * dt)) .- id- let gsnp3 = gsnp2 & updateCameraAngle- let cxtnp3 = cxtnp2-- -- Physically expend dt to advance the ball's position by its velocity,- -- handling physics collisions.- (gsnp4, cxtnp4) <- returnAWithoutMiddle ||| stepGameBallPhysics -< if' (not isPlayingState) Left Right $ (gsnp3, dt, cxtnp3)-- -- Identify output.- let gs = gsnp4- let cxt = cxtnp4-- -- Return.- returnA -< (gs, cxt)- where- returnAWithoutMiddle = arr $ \(a, _b, c) -> (a, c)---- | Step the debug free camera.------ Check whether this mode is enabled before using this wire.-stepGameClockDebugFreeCamera :: Wire ImmutaballM (GameState, Double, IBStateContext) (GameState, IBStateContext)-stepGameClockDebugFreeCamera = proc (gsn, dt, cxtn) -> do- let gsa = mkGameStateAnalysis cxtn gsn- let netMov = gsa^.gsaNetRightForwardJump- let (netRight, netForward, netJump) = netMov-- -- FRP architecture note: we have access to both 1) local state (FRP) and- -- 2) the relatively global game state. If we wanted, we could use FRP to- -- make a wire (time-varying value) that outputs the free camera position- -- offset value, and just unconditionally set the game state, disregarding- -- its input. We could also instead not use FRP but just calculate the- -- next state.- --- -- To demonstrate capabilities, I instead take a third, hybrid approach:- -- both. Use FRP to calculate a diff locally, and then apply that diff to- -- the state (relatively) globally.-- let (unrotatedNetMovementVec :: Vec3 Double) = Vec3 (fromIntegral netRight) (fromIntegral netForward) (fromIntegral netJump)- let (netMovementVec :: Vec3 Double) = tilt3ySimple (v3normalize $ (gsa^.gsaView.mviewTarget) `minusv3` (gsa^.gsaView.mviewPos)) `mv3` unrotatedNetMovementVec- -- relativeNetMovementVec is not needed: netMovementVec _already_ includes the free camera rotation, not just the original orientation in the level file.- --let (relativeNetMovementVec :: Vec3 Double) = aimVert3DSimple (Just $ 0.99*tau) (gsn^.gsDebugState.gdsCameraAimUpRadians) . aimHoriz3DSimple (gsn^.gsDebugState.gdsCameraAimRightRadians) $ netMovementVec- let netMovementVec' = if' (not freeCameraRelative) unrotatedNetMovementVec $ netMovementVec- posOffset <- integrate zv3 -< (dt * freeCameraSpeed) `sv3` netMovementVec'-- -- FRP (local).- posOffsetDiff <- differentiate -< posOffset- -- State (global).- let posOffsetUpdate = gsDebugState.gdsCameraPosOffset %~ (+ posOffsetDiff)- let gsnp1 = gsn & posOffsetUpdate-- -- Identity output.- let gs = gsnp1- let cxt = cxtn-- -- Return.- returnA -< (gs, cxt)-- where- -- Units per second.- freeCameraSpeed :: Double- freeCameraSpeed = 2.0-- -- Relative to aim? This is normal behavior, but temporarily setting- -- this to False is an option.- freeCameraRelative :: Bool- freeCameraRelative = True---- | Expend dt to step the ball through the physical world.------ Apply gravity and handle collisions.-stepGameBallPhysics :: Wire ImmutaballM (GameState, Double, IBStateContext) (GameState, IBStateContext)-stepGameBallPhysics = proc (gsn, dt, cxtn) -> do- let gsa = mkGameStateAnalysis cxtn gsn-- let x'cfg = cxtn^.ibContext.ibStaticConfig-- -- Find the gravity vector.- let gravityVectorUnrotated = (gsa^.gsaUpVec) & z3 %~ negate -- Mirror on xy plane.- let gravityVector = rotatexySimple (gsn^.gsCameraAngle) `mv3` gravityVectorUnrotated- -- Don't apply the gravity vector yet, since we might apply too much dt,- -- e.g. before the ball rests back on the floor; this could make the ball- -- bouncy, especially depending on the frame rate.- {-- let gravityAcceleration = x'cfg^.x'cfgGravity- let updateGravity =- (gsBallVel %~ (+ (dt * gravityAcceleration) `sv3` gravityVector)) .- id- let gsnp1 = gsn & updateGravity- -}- let gsnp1 = gsn- let cxtnp1 = cxtn-- -- Advance the ball through the physical world, handling collisions.- --- -- Expend dt to apply its velocity to the position, handling collisions by- -- applying reflections.- let (ballPos', ballVel') = physicsBallAdvance x'cfg (gsnp1^.gsSol) (gsnp1^.gsSolAnalysis.saPhysicsAnalysis) (gsnp1^.gsBallRadius) gravityVector dt (gsnp1^.gsBallPos) (gsnp1^.gsBallVel)- let updateBall =- (gsBallPos .~ ballPos') .- (gsBallVel .~ ballVel') .- id- let (gsnp2, cxtnp2) = (gsnp1 & updateBall, cxtnp1)-- -- Identify output.- let gs = gsnp2- let cxt = cxtnp2-- -- Return.- returnA -< (gs, cxt)---- | Expend dt to step the ball through the physical world, handling collisions.-physicsBallAdvance :: StaticConfig -> LevelIB -> SolPhysicsAnalysis -> Double -> Vec3 Double -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)-physicsBallAdvance x'cfg level spa ballRadius gravityVector dt ballPos ballVel = choice_ x'cfg level spa ballRadius gravityVector dt ballPos ballVel- where- choice_ :: StaticConfig -> LevelIB -> SolPhysicsAnalysis -> Double -> Vec3 Double -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)- --choice_ = physicsBallAdvanceStationary- --choice_ = physicsBallAdvanceGhostly- choice_ = physicsBallAdvanceBruteForce---- | For debugging or performance checking, keep the ball stationary.-physicsBallAdvanceStationary :: StaticConfig -> LevelIB -> SolPhysicsAnalysis -> Double -> Vec3 Double -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)-physicsBallAdvanceStationary _x'cfg _level _spa _ballRadius _gravityVector _dt p0 v0 = (p1, v0)- where- p1 = p0---- | For debugging or performance checking, ignore all collision checking.-physicsBallAdvanceGhostly :: StaticConfig -> LevelIB -> SolPhysicsAnalysis -> Double -> Vec3 Double -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)-physicsBallAdvanceGhostly x'cfg _level _spa _ballRadius gravityVector dt p0 v0 = (p1, v1)- where- p1 = p0 + (dt `sv3` v1)- v1 = v0 + ((dt * gravityAcceleration) `sv3` gravityVector)- gravityAcceleration = x'cfg^.x'cfgGravity---- | This version completely ignores the BSP. It checks collisions with every--- lump every frame.-physicsBallAdvanceBruteForce :: StaticConfig -> LevelIB -> SolPhysicsAnalysis -> Double -> Vec3 Double -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)-physicsBallAdvanceBruteForce = physicsBallAdvanceBruteForceCompute 0 0.0 0.0---- | Finish computing 'physicsBallAdvanceBruteFroce'.------ Make a line segment from p0 to p1, where p1 is p0 + dt*v, i.e. the tentative--- end-point of the ball's position after the frame, if no collisions occur.--- Check all lumps for a closest collision, and if one is found, expend enough--- dt to advance the ball to the point of intersection, and then reflect v0--- about the face's plane, and repeat, checking again for all lumps using the--- remaining dt to expend. Since the path represent the center of the ball,--- use the sphere's radius to check for distance. For edges and vertices, use--- as the plane a constructed plane where the plane is orthogonal to the line--- from the (new) ball center to the point of intersection. Once no collisions--- have been detected, freely expend the remaining dt. We now have the new--- ball position, at the last p1.------ Parameters:------ numCollisions:--- for testing squishes, this is incremented each collision, and once it--- exceeds the maximum, the squish condition is detected and the physics--- engine will skip any remaining collisions, letting the ball go through lumps.--- thresholdTimeRemaining:--- Once this much dt has been expended, reset the 2 squish detection--- parameters (this and numCollisions).--- thresholdRDistanceRemaining:--- Once the ball has traveled this much distance in terms of ball radiuses,--- reset the squish state in this condition too. We don't want the ball the--- ghost through walls if the frame is slow and processing a lot of dt at--- once, simply because it made many collisions in a frame even though it--- traveled a long distance (e.g. spinning around in a cone if the system--- briefly pauses), so we have thresholdTimeRemaining; and we also have--- distance because otherwise the ball can always go fast enough so that it--- bounces enough times between lumps and ghosts through.-physicsBallAdvanceBruteForceCompute :: Integer -> Double -> Double -> StaticConfig -> LevelIB -> SolPhysicsAnalysis -> Double -> Vec3 Double -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)---physicsBallAdvanceBruteForceCompute numCollisions thresholdTimeRemaining thresholdRDistanceRemaining x'cfg level spa ballRadius gravityVector dt p0 v0 =-physicsBallAdvanceBruteForceCompute numCollisions thresholdTimeRemaining thresholdRDistanceRemaining x'cfg level spa ballRadius gravityVector dt p0 v0- -- Redundantly ensure dt is not zero or negative.- | dt <= 0 + smallNum = let (p1, v1) = (p0, v0) in (p1, v1)- -- Check the optional maxPhysicsStepTime config.- | Just maxPhysicsStepTime <- x'cfg^.x'cfgMaxPhysicsStepTime, dt > maxPhysicsStepTime + smallNum =- let (p1, v1) =- physicsBallAdvanceBruteForceCompute- numCollisions- thresholdTimeRemaining- thresholdRDistanceRemaining- x'cfg- level- spa- ballRadius- gravityVector- maxPhysicsStepTime- p0- v0 in- physicsBallAdvanceBruteForceCompute- numCollisions- thresholdTimeRemaining- thresholdRDistanceRemaining- x'cfg- level- spa- ballRadius- gravityVector- (dt - maxPhysicsStepTime)- p1- v1- -- Now step the physics.- | otherwise =- case closestLumpIntersecting of -- Find the next collision.- Nothing -> -- No more collisions this frame.- -- Gravity: apply gravity to the rest of the path.- let (p1, v1) = (p0 + (dt `sv3` v0), v0g dt) in (p1, v1) -- Expend the rest of dt after the last collision.- Just (_lastLi', edt, p0', v0') -> -- Found the next collision. Expend ‘edt’ to advance the pall to p0'.- -- Gravity: only apply the gravity vector through to the next collision, later on when we produce what is v0' here.- let travelRDistance = (p0' - p0)^.r3 / ballRadius; trd = travelRDistance in- physicsBallAdvanceBruteForceCompute- ( if' resetSquishState 0 (numCollisions + 1) )- ( if' resetSquishState (x'cfg^.x'cfgMaxFrameCollisionsDtThreshold - edt) (thresholdTimeRemaining - edt) )- ( if' resetSquishState (x'cfg^.x'cfgMaxFrameCollisionsRDistanceThreshold - edt) (thresholdRDistanceRemaining - trd) )- x'cfg- level- spa- ballRadius- gravityVector- (dt - edt)- p0'- v0'-- where- bounceReturn = x'cfg^.x'cfgBounceReturn- gravityAcceleration = x'cfg^.x'cfgGravity-- resetSquishState- | thresholdTimeRemaining <= 0 = True- | thresholdRDistanceRemaining <= 0 = True- | otherwise = False-- -- | Find the closest lump intersecting the ball's path, for collisions.- closestLumpIntersectingRaw :: Maybe (Int32, Double, Vec3 Double, Vec3 Double)- --closestLumpIntersectingRaw = safeHead . sortOn (^._2) . catMaybes . toList $ lumpsIntersecting- closestLumpIntersectingRaw = safeHead . sortOn (^._2) . catMaybes $ lumpsIntersecting-- -- | Check cfgMaxFrameCollisions, and whether dt is already exhausted.- closestLumpIntersecting :: Maybe (Int32, Double, Vec3 Double, Vec3 Double)- closestLumpIntersecting- | numCollisions >= (x'cfg^.x'cfgMaxFrameCollisions) = Nothing- | dt <= 0 || (dt - 0) `equivalentSmall` 0 = Nothing- | otherwise = closestLumpIntersectingRaw-- -- | Map each lump into the closest collision, and get 1) the dt- -- needed to expend to advance the ball to this point of collision, 2)- -- the new ball pos and 3) ball vel.- --- -- Only the closest collision will be used for advancing the ball,- -- before checking for collision again afterwards.- --lumpsIntersecting :: Array Int32 (Maybe (Double, Vec3 Double, Vec3 Double))- --lumpsIntersecting = level^.solLv <&> \lump ->- lumpsIntersecting :: [Maybe (Int32, Double, Vec3 Double, Vec3 Double)]- lumpsIntersecting = flip fmap (zip [0..] (toList $ level^.solLv)) . uncurry $ \li lump ->- let- checkVertices :: Bool- -- I suspect checking the length of facesIntersectingNoBounds- -- can introduce a space leak (e.g. try out retour de force 2),- -- but it turns out probably checking the length of- -- edgesIntersecting is probably better anyway.- --checkVertices | (_:_:_:_) <- facesIntersectingNoBounds = True | otherwise = False- -- TODO FIXME: verify this also isn't broken, but for now just- -- always check verts.- --checkVertices | (_:_:_) <- edgesIntersecting = True | otherwise = False- checkVertices = True- verticesIntersecting :: [(Int32, Double, Vec3 Double, Vec3 Double)]- verticesIntersecting = if' (not checkVertices) [] $ do- -- For each verte,- vi <- indirection <$> [lump^.lumpV0 .. lump^.lumpV0 + lump^.lumpVc - 1]- let vertex = (level^.solVv) ! vi- let v = vertex^.vertP-- -- Find the distance between the path (lp) and the vertex.- let vd = abs $ line3PointDistance lp v-- -- Skip if the _infinite_ line extension from lp and- -- the vertex are too far away. This allows an early check- -- and also to ensure the call to line3DistanceCoordFromPoint is valid.- guard $ vd <= ballRadius-- -- Find the closest point coord on lp to the vertex.- let lpClosestX = line3PointCoord lp v-- -- Find how far away the candidates for x (where the ball- -- is exactly ballRadius away from the vertex) are from- -- closestX.- let lpCoordOffset = line3DistanceCoordFromPoint lp v ballRadius-- -- Find the coord on lp where the distance is the ball's- -- radius. Skip if we have nothing on lp.- let xCandidates = [lpClosestX - lpCoordOffset, lpClosestX + lpCoordOffset]- (x:_) <- return . sort . filter (\x -> 0 <= x + smallNum && x - smallNum <= 1) $ xCandidates-- -- Skip if x is not on lp. (Already done by the filter above.)- --guard $ 0 <= x + smallNum && x - smallNum <= 1-- -- Find the ball intersection point.- let ballIntersection = line3Lerp lp x `v3orWith` (lp^.p0l3)-- -- For the reflecting plane for the ball's velocity,- -- construct a virtual plane essentially with the vector- -- from the vertex to the ball's position at intersection- -- as normal.- let virtualPlane = normalPlane3 (v3normalize $ ballIntersection - v) 0-- -- Make sure the ball is going towards the vertex, not away- -- from it, similar to the normal check for planes for- -- avoiding multiple collisions in a single step for me- -- thasme lump.- guard $ (lp^.a0l3) `d3` (virtualPlane^.abcp3) <= 0-- -- Return the results of this collision, which is used if- -- it is found to be the first potential collision on the- -- path lp.- let edt = x * dt- let p0' = ballIntersection- let v0' = plane3ReflectPointAmount (virtualPlane & dp3 .~ 0) (v0g edt) (bounceReturn) -- v0g: Apply gravity for this path.- return $ (li, edt, p0', v0')-- checkEdges :: Bool- -- TODO FIXME: length >= 2 seems to cause edge collision detection- -- to not work. For now just disable this check.- --checkEdges | (_:_:_) <- facesIntersectingNoBounds = True | otherwise = False- checkEdges = True- edgesIntersecting :: [(Int32, Double, Vec3 Double, Vec3 Double)]- edgesIntersecting = if' (not checkEdges) [] $ do- -- For each edge,- ei <- indirection <$> [lump^.lumpE0 .. lump^.lumpE0 + lump^.lumpEc - 1]- let edge = (level^.solEv) ! ei- let (vi, vj) = (((level^.solVv) ! (edge^.edgeVi))^.vertP, ((level^.solVv) ! (edge^.edgeVj))^.vertP)- let el = line3Points vi vj-- -- Find the distance between the path (lp) and the edge.- let ed = abs $ line3Line3Distance lp el-- -- Skip if _the infinite lines_ are too far away. However,- -- the line segments may still be too far away even if the- -- infinite lines are not, so we'll filter lp coords in [0, 1].- guard $ ed <= ballRadius-- -- Find the closest point.- Just (lpx, elx) <- return $ line3Line3ClosestCoords lp el- let elv = line3Lerp el elx-- -- Find the point on lp where the ball is at when it first- -- collides with the edge.- let lpCoordOffset = abs $ line3DistanceCoordFromPoint lp elv ballRadius-- -- Find candidates for x: they must be within [0, 1]; if no- -- candidates pass, the line segment (the path) is too far- -- away from the edge, so we'll skip this edge.- let xCandidates = [lpx - lpCoordOffset, lpx + lpCoordOffset]- (x:_) <- return . sort . filter (\x -> 0 <= x + smallNum && x - smallNum <= 1) $ xCandidates-- let ballIntersection = line3Lerp lp x `v3orWith` (lp^.p0l3)-- let edgePointBallIntersection = line3Lerp el . line3PointCoord el $ ballIntersection-- -- Check to make sure the intersection is actually on the- -- edge _line segment_, not to leave it checking its- -- infinite line only!- let intersectionElx = line3PointCoord el edgePointBallIntersection- guard $ 0 <= intersectionElx + smallNum && intersectionElx - smallNum <= 1-- -- For the reflecting plane for the ball's velocity,- -- construct a virtual plane essentially with the vector- -- from the edge to the ball's position at intersection as- -- normal.- let virtualPlane = normalPlane3 (v3normalize $ ballIntersection - edgePointBallIntersection) 0-- -- Make sure the ball is going towards the edge, not away- -- from it, similar to the normal check for planes for- -- avoiding multiple collisions in a single step for me- -- thasme lump.- guard $ (lp^.a0l3) `d3` (virtualPlane^.abcp3) <= 0-- -- Return the results of this collision, which is used if- -- it is found to be the first potential collision on the- -- path lp.- let edt = x * dt- let p0' = ballIntersection- let v0' = plane3ReflectPointAmount (virtualPlane & dp3 .~ 0) (v0g edt) (bounceReturn) -- v0g: Apply gravity for this path.- return $ (li, edt, p0', v0')-- -- | Find all faces that intersect p0->p1.- --- -- Find where the p0->p1 intersects a side's plane. However,- -- handle the bounds of the face: if the face's plane's point- -- of intersection with p0->p1 is not behind (<=) all other- -- planes, then it's beyond the edges bordering the side.- --- -- Note this requires that all sides have a normal pointing- -- _away_ from the convex lump inside the level file.- facesIntersecting :: [(Int32, Double, Vec3 Double, Vec3 Double)]- facesIntersecting = facesIntersecting' False- _facesIntersectingNoBounds :: [(Int32, Double, Vec3 Double, Vec3 Double)]- _facesIntersectingNoBounds = facesIntersecting' True- -- | We use ignoreEdgeBounds when checking for edge and vertex- -- intersections.- facesIntersecting' :: Bool -> [(Int32, Double, Vec3 Double, Vec3 Double)]- facesIntersecting' ignoreEdgeBounds = do- let errMsg = "Internal error: physicsBallAdvanceBruteForceCompute: sides data missing for lump with li " ++ (show li) ++ "."- let sidePlanes = flip M.lookup (spa^.spaLumpPlanes) li `morElse` error errMsg-- -- For each side (check useDirectSol for which set of sides to use),- (sidx, sidePlane) <-- if useDirectSol- then do- -- For each side,- si <- indirection <$> [lump^.lumpS0 .. lump^.lumpS0 + lump^.lumpSc - 1]- let side = (level^.solSv) ! si- -- Get its plane.- let sidePlane = normalPlane3 (side^.sideN) (side^.sideD)- return (si, sidePlane)- else do- (sidx, sidePlane) <- zip [0..] sidePlanes- return (sidx, sidePlane)- -- Find where on lp it intersects; abort this try if it doesn't.- Just x <- return $ line3CoordAtDistancePlane3 sidePlane lp ballRadius- -- Only consider intersections on the line segment.- --guard $ (0 <= x + smallNum && x - smallNum <= 1)- -- The right side prevents the ball from ghost-glitching- -- through a wall for very short 'lp' ('onPlaneCheck').- let intersectsLp = 0 <= x + smallNum && x - smallNum <= 1- let onPlaneCheck = plane3PointDistance sidePlane (lp^.p0l3) `near` ballRadius && plane3PointDistance sidePlane (lp^.p1l3) `near` ballRadius- guard $ intersectsLp || onPlaneCheck-- -- Get the point in 3D space: this is where the ball would- -- be if advanced to this intersection.- let ballIntersection = line3Lerp lp x `v3orWith` (lp^.p0l3)- -- Get the point on the plane where this collision would- -- take place: the closest point on the plane to- -- 'ballIntersection'.- let planeIntersection = pointToPlane ballIntersection sidePlane- -- Only consider intersections whose plane intersection- -- points are behind all other sides.- guard . (if' ignoreEdgeBounds (const True) id) . and $ do- if useDirectSol- then do- let si = sidx-- -- For every other side …- sj <- [lump^.lumpS0 .. lump^.lumpS0 + lump^.lumpSc - 1]- guard $ sj /= si- let sidej = (level^.solSv) ! sj- let sidejPlane = normalPlane3 (sidej^.sideN) (sidej^.sideD)-- -- Make sure the point is behind this plane.- return $ plane3PointDistance sidejPlane planeIntersection <= 0- else do- -- For every other side …- (sjidx, sidejPlane) <- zip [0..] sidePlanes- guard $ sjidx /= sidx-- -- Make sure the point is behind this plane.- return $ plane3PointDistance sidejPlane planeIntersection <= 0-- -- Finally, make sure we only register a collision if the- -- ball is going towards this plane, not away from it: e.g.- -- you can only collide against a top face from above, not- -- from underneath. This prevents the same lump causing- -- multiple collision events for what should be a single- -- collision. We can do this by making sure the dot- -- product of the direction (axis) of ‘lp’ and the plane- -- normal is not positive.- guard $ (lp^.a0l3) `d3` (sidePlane^.abcp3) <= 0-- -- We've found an intersection. Now calculate the values- -- we would need if we ended up picking this after finding it- -- is indeed the closest.-- let edt = x * dt- let p0' = ballIntersection- let v0' = plane3ReflectPointAmount (sidePlane & dp3 .~ 0) (v0g edt) (bounceReturn) -- v0g: Apply gravity for this path.- return $ (li, edt, p0', v0')-- where- useDirectSol = False-- -- | Lower dimensionalities appear before higher- -- dimensionalities for equal distances. 'sortOn' is a stable- -- sort, so we can use it.- allIntersecting = concat $- [- verticesIntersecting,- edgesIntersecting,- facesIntersecting- ]-- sortedIntersecting = sortOn (^._2) $ allIntersecting-- lumpComponentIntersecting :: Maybe (Int32, Double, Vec3 Double, Vec3 Double)- lumpComponentIntersecting = safeHead sortedIntersecting-- -- Make sure the lump isn't the one we last used for collisions.- r = lumpComponentIntersecting- in- r-- where- -- | Draw a line segment from the ball's position (center of- -- sphere) to the tentative end-point if all dt were to be- -- expended now using its current velocity.- --- -- TODO: double check edge cases of small lp don't cause- -- issues. The guard has a p0 and p1 nearness check, but there- -- might still be issues.- lp = let p1 = p0 + (dt `sv3` v0) in line3Points p0 p1-- -- | It seems sols have indirection for lump sides, vertices, and- -- edges, but not edge indices to vertices.- indirection :: Int32 -> Int32- indirection idx = (level^.solIv) ! idx-- v0g edt = v0 + ((edt * gravityAcceleration) `sv3` gravityVector) -- What velocity is if v0 has time added to gravity.+{-# LANGUAGE TemplateHaskell, Arrows, ScopedTypeVariables, NondecreasingIndentation #-}++-- | Game state and immutaball state interface.+module Immutaball.Ball.State.Game+ (+ GameRequest(..), giRequest, giGameState, giIBStateContext, grRequest,+ GameResponse(..), goGameEvents, goGameState, goIBStateContext, grGameEvents,+ GameEvent(..), AsGameEvent(..),++ NextCollision(..), ncClosestLump, ncCheckLumps, ncDistanceTo, ncTimeTo,+ NextCollisionState(..), ncsNc, ncsBspsLeft,+ LumpBSPPartitionParent(..), lbspppParent, lbspppIsRightBranch,+ LumpLpPlaneIntersection(..), AsLumpLpPlaneIntersection(..),+ LumpLpPlaneCommonIntersection(..), llpciPriority, llpciDistance,+ llpciTimeTo, llpciLi, llpciBi, llpciBodyTranslation, llpciLp,+ llpciIntersection, llpciIntersectionLx, llpciIntersectionBall,+ llpciVirtualPlane,+ LumpLpPlaneFaceIntersection(..), llpfiPlaneIdx, llpfiPlane, llpfiDistance,+ llpfiTimeTo, llpfiLi, llpfiBi, llpfiLp, llpfiBodyTranslation,+ llpfiIntersection, llpfiIntersectionLx, llpfiIntersectionBall,+ LumpLpPlaneEdgeIntersection(..), llpeiDistance, llpeiTimeTo, llpeiLi,+ llpeiBi, llpeiBodyTranslation, llpeiLp, llpeiIntersection,+ llpeiIntersectionLx, llpeiIntersectionBall, llpeiVirtualPlane,+ LumpLpPlaneVertIntersection(..), llpviDistance, llpviTimeTo, llpviLi,+ llpviBi, llpviBodyTranslation, llpviLp, llpviIntersection,+ llpviIntersectionLx, llpviIntersectionBall, llpviVirtualPlane,+ NextCollisionLump(..), nclLumpi, nclLpPlaneIntersections,++ lpxc,+ lpxcomp,+ nclcomp,+ stepGame,+ rendererTransformationMatrix,+ renderBall,+ renderBallSetup,+ stepGameInput,+ stepGameInputMovement,+ stepGameInputEvents,+ stepGameClock,+ stepGameClockDebugFreeCamera,+ stepGameBallPhysics,++ physicsBallAdvance,+ physicsBallAdvanceDefault,+ physicsBallAdvanceStationary,+ physicsBallAdvanceGhostly,+ physicsBallAdvanceBruteForce,+ physicsBallAdvanceBruteForceCompute,++ physicsBallAdvanceBSP,++ -- * Utils.+ getPathTranslation,+ getBodyTranslation++ -- * Local utils.+ ) where++import Prelude ()+import Immutaball.Prelude++import Control.Monad+import Data.Bits+import Data.Int+import Data.Foldable+import Data.Function hiding (id, (.))+import Data.List+import qualified Data.Map.Lazy as M+import Data.Maybe++import Control.Arrow+import Control.Lens+import Control.Monad.Trans.Class+import Control.Monad.Trans.State.Lazy+import Data.Array.IArray+import qualified Data.Set as S+import Graphics.GL.Compatibility45+--import Graphics.GL.Core45+import Graphics.GL.Types+import qualified SDL.Raw.Enum as Raw++import Data.LabeledBinTree+import Immutaball.Ball.Game+import Immutaball.Share.Config+import Immutaball.Share.Context+import Immutaball.Share.ImmutaballIO.GLIO+import Immutaball.Share.Level+import Immutaball.Share.Level.Analysis+import Immutaball.Share.Math+import Immutaball.Share.SDLManager+import Immutaball.Share.State+import Immutaball.Share.State.Context+import Immutaball.Share.Utils+import Immutaball.Share.Video+import Immutaball.Share.Wire++-- TODO: fully implement.++data GameRequest = GameRequest {+ _giRequest :: Request,+ _giGameState :: GameState,+ _giIBStateContext :: IBStateContext+}+makeLenses ''GameRequest+grRequest :: Lens' GameRequest Request+grRequest = giRequest++data GameResponse = GameResponse {+ _goGameEvents :: [GameEvent],+ _goGameState :: GameState,+ _goIBStateContext :: IBStateContext+}+--makeLenses ''GameResponse++data GameEvent =+ NewGameMode GameMode+ -- | PlaceholderEvent+makeLenses ''GameResponse+makeClassyPrisms ''GameEvent++grGameEvents :: Lens' GameResponse [GameEvent]+grGameEvents = goGameEvents++-- | Used by 'physicsBallAdvanceBSP'.+data NextCollision = NextCollision {+ _ncClosestLump :: NextCollisionLump,+ _ncCheckLumps :: [NextCollisionLump], -- ^ Not necessarily sorted.+ _ncDistanceTo :: Double,+ _ncTimeTo :: Double+}+--makeLenses ''NextCollision++-- | Used by 'physicsBallAdvanceBSP'.+data NextCollisionState = NextCollisionState {+ _ncsNc :: Maybe NextCollision,+ _ncsBspsLeft :: [(Int32, Tree LumpBSPPartition, Maybe LumpBSPPartitionParent)]+}+--makeLenses ''NextCollisionState++-- | Used by 'physicsBallAdvanceBSP'.+data LumpBSPPartitionParent = LumpBSPPartitionParent {+ -- | The parent of this partition.+ _lbspppParent :: LumpBSPPartition,+ -- | Are we a right branch of the parent, rather than left?+ _lbspppIsRightBranch :: Bool+}+--makeLenses ''LumpBSPPartitionParent++-- | Used by 'physicsBallAdvanceBSP'.+data NextCollisionLump = NextCollisionLump {+ _nclLumpi :: Int32,+ _nclLpPlaneIntersections :: [LumpLpPlaneIntersection] -- Assumed to be non-empty and sorted.+}+--makeLenses ''NextCollisionLump++-- | A face, edge, or vertex intersection.+-- See 'LumpLpPlaneFaceIntersection' for more information.+--+-- Used by 'physicsBallAdvanceBSP'.+data LumpLpPlaneIntersection =+ FaceIntersection LumpLpPlaneFaceIntersection+ | EdgeIntersection LumpLpPlaneEdgeIntersection+ | VertIntersection LumpLpPlaneVertIntersection+--makeClassyPrisms ''LumpLpPlaneIntersection++-- | Common fields for the 3 types of 'LumpLpPlaneInterection's.+--+-- Used by 'physicsBallAdvanceBSP'.+data LumpLpPlaneCommonIntersection = LumpLpPlaneCommonIntersection {+ -- | The collision type priority: 1 for vertex, 2 for edge, 3 for plane.+ -- Lowest priority should be sorted first if 2 intersection points are+ -- within threshold distance; otherwise, they should be sorted be distance.+ _llpciPriority :: Integer,+ -- | The distance along lp to the plane.+ _llpciDistance :: Double,+ -- | The dt that would be expended to reach the point of intersection.+ _llpciTimeTo :: Double,++ -- | The lump index.+ _llpciLi :: Int32,+ -- | The body index.+ _llpciBi :: Int32,+ -- | For convenience, store the current body translation (where it is on+ -- the path - you can add the base vertices to get the net world coords.)+ _llpciBodyTranslation :: Vec3 Double,++ -- | The lp in question (relative body coords; use 'ipb' to go back to world coords).+ _llpciLp :: Line3 Double,+ -- | The point of intersection on the body, relative to body (use ‘ipb’ to go back).+ _llpciIntersection :: Vec3 Double,++ -- | Where on ‘lp’ the intersection happens (0.0 if lp is really small).+ _llpciIntersectionLx :: Double,+ -- | Where the ball would be positioned at the point of intersection; body coords.+ _llpciIntersectionBall :: Vec3 Double,++ -- | The plane, or virtual plane, for the collision.+ _llpciVirtualPlane :: Plane3 Double+}+--makeLenses ''LumpLpPlaneCommonIntersection++-- | When considering an advancement of the ball, for a given lump, and for a+-- given plane on that lump, a value of this type represents information about+-- the intersection of the lp line for the ball and this plane.+--+-- Used by 'physicsBallAdvanceBSP'.+data LumpLpPlaneFaceIntersection = LumpLpPlaneFaceIntersection {+ -- | The lump plane index for the plane in question.+ _llpfiPlaneIdx :: Integer,+ -- | The plane itself (relative body coords).+ _llpfiPlane :: Plane3 Double,+ -- | The distance along lp to the plane.+ _llpfiDistance :: Double,+ -- | The dt that would be expended to reach this point of intersection.+ _llpfiTimeTo :: Double,++ -- | The lump index.+ _llpfiLi :: Int32,+ -- | The body index.+ _llpfiBi :: Int32,+ -- | For convenience, store the current body translation (where it is on+ -- the path - you can add the base vertices to get the net world coords.)+ _llpfiBodyTranslation :: Vec3 Double,++ -- | The lp in question (relative body coords; use 'ipb' to go back to world coords).+ _llpfiLp :: Line3 Double,+ -- | The point of intersection on the body, relative to body (use ‘ipb’ to go back).+ _llpfiIntersection :: Vec3 Double,++ -- | Where on ‘lp’ the intersection happens (0.0 if lp is really small).+ _llpfiIntersectionLx :: Double,+ -- | Where the ball would be positioned at the point of intersection; body coords.+ _llpfiIntersectionBall :: Vec3 Double+}+--makeLenses ''LumpLpPlaneFaceIntersection++-- | Used by 'physicsBallAdvanceBSP'.+data LumpLpPlaneEdgeIntersection = LumpLpPlaneEdgeIntersection {+ -- | The distance along lp to the plane.+ _llpeiDistance :: Double,+ -- | The dt that would be expended to reach the point of intersection.+ _llpeiTimeTo :: Double,++ -- | The lump index.+ _llpeiLi :: Int32,+ -- | The body index.+ _llpeiBi :: Int32,+ -- | For convenience, store the current body translation (where it is on+ -- the path - you can add the base vertices to get the net world coords.)+ _llpeiBodyTranslation :: Vec3 Double,++ -- | The lp in question (relative body coords; use 'ipb' to go back to world coords).+ _llpeiLp :: Line3 Double,+ -- | The point of intersection on the body, relative to body (use ‘ipb’ to go back).+ _llpeiIntersection :: Vec3 Double,++ -- | Where on ‘lp’ the intersection happens (0.0 if lp is really small).+ _llpeiIntersectionLx :: Double,+ -- | Where the ball would be positioned at the point of intersection; body coords.+ _llpeiIntersectionBall :: Vec3 Double,++ -- | The plane, or virtual plane, for the collision.+ _llpeiVirtualPlane :: Plane3 Double+}+--makeLenses ''LumpLpPlaneEdgeIntersection++-- | Used by 'physicsBallAdvanceBSP'.+data LumpLpPlaneVertIntersection = LumpLpPlaneVertIntersection {+ -- | The distance along lp to the plane.+ _llpviDistance :: Double,+ -- | The dt that would be expended to reach the point of intersection.+ _llpviTimeTo :: Double,++ -- | The lump index.+ _llpviLi :: Int32,+ -- | The body index.+ _llpviBi :: Int32,+ -- | For convenience, store the current body translation (where it is on+ -- the path - you can add the base vertices to get the net world coords.)+ _llpviBodyTranslation :: Vec3 Double,++ -- | The lp in question (relative body coords; use 'ipb' to go back to world coords).+ _llpviLp :: Line3 Double,+ -- | The point of intersection on the body, relative to body (use ‘ipb’ to go back).+ _llpviIntersection :: Vec3 Double,++ -- | Where on ‘lp’ the intersection happens (0.0 if lp is really small).+ _llpviIntersectionLx :: Double,+ -- | Where the ball would be positioned at the point of intersection; body coords.+ _llpviIntersectionBall :: Vec3 Double,++ -- | The plane, or virtual plane, for the collision.+ _llpviVirtualPlane :: Plane3 Double+}+--makeLenses ''LumpLpPlaneVertIntersection++makeLenses ''NextCollision+makeLenses ''NextCollisionState+makeLenses ''LumpBSPPartitionParent+makeClassyPrisms ''LumpLpPlaneIntersection+makeLenses ''LumpLpPlaneCommonIntersection+makeLenses ''LumpLpPlaneFaceIntersection+makeLenses ''LumpLpPlaneEdgeIntersection+makeLenses ''LumpLpPlaneVertIntersection+makeLenses ''NextCollisionLump++-- | Access lump lp plane intersections through a common interface.+--+-- Pseudo-lens, since priority updates are ignored.+--+-- Used by 'physicsBallAdvanceBSP'.+lpxc :: Lens' LumpLpPlaneIntersection LumpLpPlaneCommonIntersection+lpxc = lens getter (flip setter)+ where+ getter :: LumpLpPlaneIntersection -> LumpLpPlaneCommonIntersection+ getter (FaceIntersection lpx) = LumpLpPlaneCommonIntersection {+ _llpciPriority = 3,+ _llpciDistance = lpx^.llpfiDistance,+ _llpciTimeTo = lpx^.llpfiTimeTo,++ _llpciLi = lpx^.llpfiLi,+ _llpciBi = lpx^.llpfiBi,+ _llpciBodyTranslation = lpx^.llpfiBodyTranslation,++ _llpciLp = lpx^.llpfiLp,+ _llpciIntersection = lpx^.llpfiIntersection,++ _llpciIntersectionLx = lpx^.llpfiIntersectionLx,+ _llpciIntersectionBall = lpx^.llpfiIntersectionBall,++ _llpciVirtualPlane = lpx^.llpfiPlane+ }+ getter (EdgeIntersection lpx) = LumpLpPlaneCommonIntersection {+ _llpciPriority = 2,+ _llpciDistance = lpx^.llpeiDistance,+ _llpciTimeTo = lpx^.llpeiTimeTo,++ _llpciLi = lpx^.llpeiLi,+ _llpciBi = lpx^.llpeiBi,+ _llpciBodyTranslation = lpx^.llpeiBodyTranslation,++ _llpciLp = lpx^.llpeiLp,+ _llpciIntersection = lpx^.llpeiIntersection,++ _llpciIntersectionLx = lpx^.llpeiIntersectionLx,+ _llpciIntersectionBall = lpx^.llpeiIntersectionBall,++ _llpciVirtualPlane = lpx^.llpeiVirtualPlane+ }+ getter (VertIntersection lpx) = LumpLpPlaneCommonIntersection {+ _llpciPriority = 1,+ _llpciDistance = lpx^.llpviDistance,+ _llpciTimeTo = lpx^.llpviTimeTo,++ _llpciLi = lpx^.llpviLi,+ _llpciBi = lpx^.llpviBi,+ _llpciBodyTranslation = lpx^.llpviBodyTranslation,++ _llpciLp = lpx^.llpviLp,+ _llpciIntersection = lpx^.llpviIntersection,++ _llpciIntersectionLx = lpx^.llpviIntersectionLx,+ _llpciIntersectionBall = lpx^.llpviIntersectionBall,++ _llpciVirtualPlane = lpx^.llpviVirtualPlane+ }+ setter :: LumpLpPlaneCommonIntersection -> LumpLpPlaneIntersection -> LumpLpPlaneIntersection+ setter c (FaceIntersection lpx) = FaceIntersection $ lpx &+ (llpfiDistance .~ (c^.llpciDistance) ) .+ (llpfiTimeTo .~ (c^.llpciTimeTo) ) .++ (llpfiLi .~ (c^.llpciLi) ) .+ (llpfiBi .~ (c^.llpciBi) ) .+ (llpfiBodyTranslation .~ (c^.llpciBodyTranslation) ) .++ (llpfiLp .~ (c^.llpciLp) ) .+ (llpfiIntersection .~ (c^.llpciIntersection) ) .++ (llpfiIntersectionLx .~ (c^.llpciIntersectionLx) ) .+ (llpfiIntersectionBall .~ (c^.llpciIntersectionBall)) .++ (llpfiPlane .~ (c^.llpciVirtualPlane) ) .++ id+ setter c (EdgeIntersection lpx) = EdgeIntersection $ lpx &+ (llpeiDistance .~ (c^.llpciDistance )) .+ (llpeiTimeTo .~ (c^.llpciTimeTo )) .++ (llpeiLi .~ (c^.llpciLi )) .+ (llpeiBi .~ (c^.llpciBi )) .+ (llpeiBodyTranslation .~ (c^.llpciBodyTranslation )) .++ (llpeiLp .~ (c^.llpciLp )) .+ (llpeiIntersection .~ (c^.llpciIntersection )) .++ (llpeiIntersectionLx .~ (c^.llpciIntersectionLx )) .+ (llpeiIntersectionBall .~ (c^.llpciIntersectionBall)) .++ (llpeiVirtualPlane .~ (c^.llpciVirtualPlane )) .++ id+ setter c (VertIntersection lpx) = VertIntersection $ lpx &+ (llpviDistance .~ (c^.llpciDistance )) .+ (llpviTimeTo .~ (c^.llpciTimeTo )) .++ (llpviLi .~ (c^.llpciLi )) .+ (llpviBi .~ (c^.llpciBi )) .+ (llpviBodyTranslation .~ (c^.llpciBodyTranslation )) .++ (llpviLp .~ (c^.llpciLp )) .+ (llpviIntersection .~ (c^.llpciIntersection )) .++ (llpviIntersectionLx .~ (c^.llpciIntersectionLx )) .+ (llpviIntersectionBall .~ (c^.llpciIntersectionBall)) .++ (llpviVirtualPlane .~ (c^.llpciVirtualPlane )) .++ id++-- | Compare two collisions mostly by distance (see priorities note in+-- 'LumpLpPlaneCommonIntersection').+--+-- Used by 'physicsBallAdvanceBSP'.+lpxcomp :: LumpLpPlaneIntersection -> LumpLpPlaneIntersection -> Ordering+lpxcomp a b+ | (abs $ a'^.llpciDistance - b'^.llpciDistance) <= smallishNum =+ (a'^.llpciPriority, a'^.llpciDistance) `compare` (b'^.llpciPriority, b'^.llpciDistance)+ | otherwise =+ (a'^.llpciDistance) `compare` (b'^.llpciDistance)+ where+ (a', b') = (a^.lpxc, b^.lpxc)++-- | Convenience utility to to compare through 'lpxcomp' 2 valid+-- 'NextCollisionLumps'. (Since they're valid, the list must be sorted and+-- non-empty.+--+-- Used by 'physicsBallAdvanceBSP'.+nclcomp :: NextCollisionLump -> NextCollisionLump -> Ordering+nclcomp a b+ | (alpx:_) <- a^.nclLpPlaneIntersections+ , (blpx:_) <- b^.nclLpPlaneIntersections =+ alpx `lpxcomp` blpx+ | otherwise = error "Error: nclcomp: assumptions of non-empty lists are violated!"++-- | Step the game.+-- TODO: finish implementing.+stepGame :: Wire ImmutaballM GameRequest GameResponse+stepGame = proc gr -> do+ let request = gr^.giRequest+ let cxtn = gr^.giIBStateContext++ -- Get starting game state.+ let (gsn :: GameState) = (gr^.giGameState)++ -- Handle input (in request). (With many possibly input requests, stepGameInput itself handles delegation of this.)+ (gsnp1, cxtnp1) <- stepGameInput -< (gsn, request, cxtn)++ -- Step the game on clock request. (Only one request is clock; handle it here it.)+ mdt <- returnA -< const Nothing ||| Just $ matching _Clock request+ (gsnp2, cxtnp2) <- returnA ||| stepGameClock -< maybe (Left (gsnp1, cxtnp1)) (\dt -> Right (gsn, dt, cxtnp1)) $ mdt++ -- Identify new game state.+ let (gs :: GameState) = gsnp2+ let (cxt :: IBStateContext) = cxtnp2++ -- Return results.+ returnA -< GameResponse {+ _goGameEvents = [], -- TODO: detect new game mode.+ _goGameState = gs,+ _goIBStateContext = cxt+ }++-- | Get a matrix that converts from world coordinates to OpenGL coordinates+-- given the view.+rendererTransformationMatrix :: IBStateContext -> GameState -> MView -> Mat4 Double+rendererTransformationMatrix cxt gs view_ = worldToGL <> rescaleDepth depthScale 0 <> viewMat' viewCollapse view_ <> tilt+ where+ x'cfg = cxt^.ibContext.ibStaticConfig+ depthScale = x'cfg^.x'cfgDepthScale+ viewCollapse = x'cfg^.x'cfgViewCollapse++ -- Translate to the ball (negated actually), rotate by gsCameraAngle, tilt3z, and untranslate.+ tilt =+ translate3 (gs^.gsBallPos) <> -- Finally, undo the first translation.+ tilt3z (gsa^.gsaUpVec) <> -- Tilt the world.+ rotatexy (-gs^.gsCameraAngle) <> -- Rotate camera.+ translate3 (-gs^.gsBallPos) -- First, go to ball (negated actually).+ gsa = mkGameStateAnalysis cxt gs++renderBall :: Wire ImmutaballM ((MView, GameState), IBStateContext) IBStateContext+renderBall = proc ((camera_, gs), cxtn) -> do+ hasInit <- delay False -< returnA True+ cxtnp1 <- arr snd ||| renderBallSetup -< if' hasInit Left Right (gs, cxtn)++ let sdlGL1' = liftIBIO . sdlGL1 (cxtnp1^.ibContext.ibSDLManagerHandle)++ -- Render the ball.+ (mballElemVaoVboEbo, cxtnp2) <- getBallElemVaoVboEbo -< cxtnp1+ let ballElemVaoVboEbo = fromMaybe (error "Internal error: renderBall expected elem vao and buf to be present, but it's missing!") mballElemVaoVboEbo+ let (ballElemVao, _ballElemVbo, _ballElemEbo) = ballElemVaoVboEbo++ -- Set the transformation matrix for the ball.+ mat <- arr $ uncurry3 rendererTransformationMatrix -< (cxtnp2, gs, camera_)+ cxtnp3 <- setTransformation -< (mat, cxtnp2)++ let (renderBallDirect :: GLIOF ()) = do+ -- Tell the shaders to disable the scene data.+ GLUniform1i shaderEnableSceneDataLocation falseAsIntegral ()+ -- Tell the shaders we are drawing the ball right now.+ GLUniform1i shaderEnableBallDataLocation trueAsIntegral ()++ -- Tell the shaders the ball radius.+ let (ballRadiusf :: GLfloat) = realToFrac $ gs^.gsBallRadius+ GLUniform1f shaderBallRadiusLocation ballRadiusf ()+ -- Tell the shaders the ball num triangles.+ let (ballNumTriangles :: Integer) = cxtnp3^.ibContext.ibStaticConfig.x'cfgBallTriangles+ GLUniform1i shaderBallNumTrianglesLocation (fromIntegral ballNumTriangles) ()+ -- Tell the shaders the ball position.+ let (ballPosX :: GLfloat, ballPosY :: GLfloat, ballPosZ :: GLfloat) = (realToFrac $ gs^.gsBallPos.x3, realToFrac $ gs^.gsBallPos.y3, realToFrac $ gs^.gsBallPos.z3)+ GLUniform3f shaderBallPosLocation ballPosX ballPosY ballPosZ ()+ -- Tell the shaders the ball rotation.+ let (ballRotX :: GLfloat, ballRotY :: GLfloat, ballRotZ :: GLfloat) = (realToFrac $ gs^.gsBallRot.x3, realToFrac $ gs^.gsBallRot.y3, realToFrac $ gs^.gsBallRot.z3)+ GLUniform3f shaderBallRotLocation ballRotX ballRotY ballRotZ ()++ -- Tell the shaders to render the ball's triangles' vertices. The+ -- shader for now will just directly draw a basic ball.+ -- (TODO: support more than just basic ball.)+ GLBindVertexArray ballElemVao ()++ -- Use the vao to tell the shader to draw the geometry.+ --let (ballNumTriangles :: Integer) = cxtnp3^.ibContext.ibStaticConfig.x'cfgBallTriangles+ GLDrawArrays GL_TRIANGLES 0 (fromIntegral (3 * ballNumTriangles)) ()++ -- Unbind the VAO.+ GLBindVertexArray 0 ()+ -- We're not using textures for the basic ball; the shader can calculate the color.+ let (assignTextures :: GLIOF ()) = return ()+ -- Enable transparency for the ball.+ let (isAlpha :: Bool) = True+ let (alphaSetup :: GLIOF ()) = do+ if isAlpha+ then do+ GLEnable GL_BLEND ()+ GLBlendEquationSeparate GL_FUNC_ADD GL_FUNC_ADD ()+ GLBlendFuncSeparate GL_SRC_ALPHA GL_ONE_MINUS_SRC_ALPHA GL_ONE GL_ZERO ()++ GLEnable GL_DEPTH_TEST ()+ GLDepthMask GL_FALSE ()+ else do+ GLDisable GL_BLEND ()++ GLEnable GL_DEPTH_TEST ()+ GLDepthMask GL_TRUE ()+ let (alphaFinish :: GLIOF ()) = do+ if isAlpha+ then do+ GLDisable GL_BLEND ()++ GLDepthMask GL_TRUE ()+ else do+ GLDisable GL_BLEND ()++ GLDepthMask GL_TRUE ()++ let (doRenderBall :: GLIOF ()) = do+ alphaSetup+ assignTextures+ renderBallDirect+ alphaFinish+ () <- monadic -< sdlGL1' doRenderBall++ let cxt = cxtnp3+ returnA -< cxt++renderBallSetup :: Wire ImmutaballM (GameState, IBStateContext) IBStateContext+renderBallSetup = proc (_gs0, cxtn) -> do+ -- A new game state hasn't had its ball render setup performed yet.+ -- Perform setup.++ -- First, set up ball elem vao and vbo. See if a previous setup has+ -- already done it. Race conditions are okay here, since setting the ball+ -- elem vao would discard an old elem vao in a race properly.+ -- (Alternatively, we could also always set it up here since it would only+ -- happen on a new game state, but checking probably saves a bit of+ -- performance for uploading the VAO.)+ (mballElemVaoVboEbo, cxtnp1) <- getBallElemVaoVboEbo -< cxtn++ -- Set up the data, ballElemVaoVboEboDataGPU.+ let (ballNumTriangles :: Integer) = cxtnp1^.ibContext.ibStaticConfig.x'cfgBallTriangles+ let (ballElemVaoVboEboData :: Array Int32 Int32) = genArray (0, 3 * fromIntegral ballNumTriangles - 1) $ \idx -> idx+ let (ballElemVaoVboEboDataGPU :: GLData) = gpuEncodeArray ballElemVaoVboEboData++ -- Upload ball elems vao and buf. (Just returnA the cxtnp1 if there's already a ball VAO.)+ cxtnp2 <- arr (^._3) ||| setBallElemVaoVboEbo -< if' (isJust mballElemVaoVboEbo) Left Right (ballElemVaoVboEboDataGPU, True, cxtnp1)++ let cxt = cxtnp2+ returnA -< cxt++-- | Handle input.+stepGameInput :: Wire ImmutaballM (GameState, Request, IBStateContext) (GameState, IBStateContext)+stepGameInput = proc (gsn, request, cxtn) -> do+ -- Handle forward, right, etc., movement.+ (gsnp1, cxtnp1) <- stepGameInputMovement -< (gsn, request, cxtn)++ -- Handle clicking to start playing, etc.+ (gsnp2, cxtnp2) <- stepGameInputEvents -< (gsnp1, request, cxtnp1)++ -- Identify output.+ let cxt = cxtnp2+ let gs = gsnp2++ -- Return.+ returnA -< (gs, cxt)++stepGameInputMovement :: Wire ImmutaballM (GameState, Request, IBStateContext) (GameState, IBStateContext)+stepGameInputMovement = proc (gsn, request, cxtn) -> do+ -- Movement.+ let update = case request of+ (Keybd char down) ->+ if' (char == cxtn^.ibNeverballrc.keyForward) (gsInputState.ginsForwardOn .~ down) .+ if' (char == cxtn^.ibNeverballrc.keyBackward) (gsInputState.ginsBackwardOn .~ down) .+ if' (char == cxtn^.ibNeverballrc.keyLeft) (gsInputState.ginsLeftOn .~ down) .+ if' (char == cxtn^.ibNeverballrc.keyRight) (gsInputState.ginsRightOn .~ down) .+ if' (char == cxtn^.ibContext.ibStaticConfig.x'cfgVertUpKey) (gsInputState.ginsVertUpOn .~ down) .+ if' (char == cxtn^.ibContext.ibStaticConfig.x'cfgVertDownKey) (gsInputState.ginsVertDownOn .~ down) $+ id+ (Click _button@Raw.SDL_BUTTON_LEFT down) -> (gsInputState.ginsMouseLeftOn .~ down)+ (Click _button@Raw.SDL_BUTTON_RIGHT down) -> (gsInputState.ginsMouseRightOn .~ down)+ _ -> id++ let gsnp1 = gsn & update++ -- Toggle debug camera view.+ let freeCamToggleUpdate = if' (not $ cxtn^.ibContext.ibStaticConfig.x'cfgDebugFreeCamera) id $+ case request of+ (Keybd char True) ->+ if' (char == cxtn^.ibContext.ibStaticConfig.x'cfgFreeCameraToggleKey) (gsDebugState.gdsCameraDebugOn %~ not) $+ id+ _ -> id++ let gsnp2 = gsnp1 & freeCamToggleUpdate++ -- Free camera aiming.+ -- mouse sense gives int in pixels for a full circle rotation, except we also scale rotation amount by curMouseSpeed (the 2 speeds are multiplied (although neverballrc is reciprocal)).+ let (curMouseSpeed :: Double) = 4.0+ let (curMouseSense :: Int) = cxtn^.ibNeverballrc.mouseSense+ let updateFreeAim = if' (not $ gsnp2^.gsDebugState.gdsCameraDebugOn) id $ case request of+ (Point _x _y dx' dy') ->+ -- SDL for me reported inverted dy (i.e. SDL's 0,0 at the top-left corner); so invert dy.+ let dx = dx' in+ let dy = -dy' in++ let (dxd :: Double) = fromIntegral dx in+ let (dyd :: Double) = fromIntegral dy in++ let (drawCirclesX :: Double) = dxd / fromIntegral curMouseSense in+ let (drawCirclesY :: Double) = dyd / fromIntegral curMouseSense in++ let (drawRadiansX :: Double) = tau * drawCirclesX in+ let (drawRadiansY :: Double) = tau * drawCirclesY in++ let (dradiansX :: Double) = curMouseSpeed * drawRadiansX in+ let (dradiansY :: Double) = curMouseSpeed * drawRadiansY in++ (gsDebugState.gdsCameraAimRightRadians %~ (+ dradiansX)) .+ (gsDebugState.gdsCameraAimUpRadians %~ (+ dradiansY)) .+ id+ _ -> id++ -- Cap: let net view be capped later in 'mkGameStateAnalysis', since there might already be a vertical aim to target.+ -- But also cap here at twice the radius to allow full movement.+ let updateCapFreeAimUp = gsDebugState.gdsCameraAimUpRadians %~ (min (2 * tau/2) . max (-2 * tau/2))++ let gsnp3 = gsnp2 & updateCapFreeAimUp . updateFreeAim++ -- Tilt the world if playing.+ -- The config for curMouseSense is interpreted to mean the number of pixels+ -- needed to traverse the entire tilt range (2 * x'cfgMaxTilt).+ let (mouseTiltModifier :: Double) = 1.0 -- Don't change mouse sensitivity for tilt.+ let (mouseSensitivity :: Int) = cxtn^.ibNeverballrc.mouseSense+ let (maxTilt :: Double) = cxtn^.ibContext.ibStaticConfig.x'cfgMaxTilt+ let updateTiltBase = if' (not . isPlaying $ gsnp1^.gsGameMode) id $ case request of+ (Point _x _y dx' dy') ->+ -- SDL for me reported inverted dy (i.e. SDL's 0,0 at the top-left corner); so invert dy.+ let dx = dx' in+ let dy = -dy' in++ let (dxd :: Double) = fromIntegral dx in+ let (dyd :: Double) = fromIntegral dy in++ let (dTiltRangesX :: Double) = dxd / fromIntegral mouseSensitivity in+ let (dTiltRangesY :: Double) = dyd / fromIntegral mouseSensitivity in++ let (dRawRadiansX :: Double) = tau * dTiltRangesX in+ let (dRawRadiansY :: Double) = tau * dTiltRangesY in++ let (dRadiansX :: Double) = mouseTiltModifier * dRawRadiansX in+ let (dRadiansY :: Double) = mouseTiltModifier * dRawRadiansY in++ (gsGravityState.gravsTiltRightRadians %~ (+ dRadiansX)) .+ (gsGravityState.gravsTiltForwardRadians %~ (+ dRadiansY)) .+ id+ _ -> id+ let updateTiltCap =+ (gsGravityState.gravsTiltRightRadians %~ (min maxTilt . max (-maxTilt))) .+ (gsGravityState.gravsTiltForwardRadians %~ (min maxTilt . max (-maxTilt))) .+ id+ let updateTilt = updateTiltCap <<< updateTiltBase++ let gsnp4 = gsnp3 & updateTilt++ -- Identify output.+ let gs = gsnp4+ let cxt = cxtn++ -- Return.+ returnA -< (gs, cxt)++-- | Handle clicking to start playing, etc.+-- TODO: generate event for new game mode.+stepGameInputEvents :: Wire ImmutaballM (GameState, Request, IBStateContext) (GameState, IBStateContext)+stepGameInputEvents = proc (gsn, request, cxtn) -> do+ -- Start playing?+ let update = if' (gsn^.gsGameMode /= Intermission) id $ case request of+ (Click _button@Raw.SDL_BUTTON_LEFT _down@True) -> (gsGameMode .~ Playing)+ _ -> id++ let gsnp1 = gsn & update++ -- Identify output.+ let gs = gsnp1+ let cxt = cxtn++ -- Return.+ returnA -< (gs, cxt)++-- | Step a frame of the game state on clock.+stepGameClock :: Wire ImmutaballM (GameState, Double, IBStateContext) (GameState, IBStateContext)+stepGameClock = proc (gsn, dt, cxtn) -> do+ let gsa = mkGameStateAnalysis cxtn gsn++ -- Step debug camera.+ (gsnp1, cxtnp1) <- returnA ||| stepGameClockDebugFreeCamera -<+ if' (not $ gsn^.gsDebugState.gdsCameraDebugOn)+ (Left (gsn, cxtn))+ (Right (gsn, dt, cxtn))++ -- Advance time elapsed if in play state.+ let cxtnp2 = cxtnp1+ let gsnp2 = gsnp1 &+ if' (not . isPlaying $ gsnp1^.gsGameMode)+ (id)+ (gsTimeElapsed %~ (+ dt))++ -- Respond to left and right mouse down by moving the camera.+ rec+ lastNetMouseRight <- delay 0 -< netMouseRight+ netMouseRight <- returnA -< gsa^.gsaNetMouseRight++ lastIsPlayingState <- delay False -< isPlayingState+ isPlayingState <- returnA -< isPlaying $ gsnp2^.gsGameMode++ foundChange <- hold False -< if' ((lastIsPlayingState == isPlayingState) && (lastNetMouseRight /= netMouseRight)) (Just True) Nothing++ let cameraAngleSpeed = 1.0 -- radians per second -- TODO: use neverballrc rotate_slow+ let updateCameraAngle = if' (not foundChange || not isPlayingState) id $+ (gsCameraAngle %~ (+ (-fromIntegral netMouseRight) * cameraAngleSpeed * dt)) .+ id+ let gsnp3 = gsnp2 & updateCameraAngle+ let cxtnp3 = cxtnp2++ -- Physically expend dt to advance the ball's position by its velocity,+ -- handling physics collisions.+ (gsnp4, cxtnp4) <- returnAWithoutMiddle ||| stepGameBallPhysics -< if' (not isPlayingState) Left Right $ (gsnp3, dt, cxtnp3)++ -- Identify output.+ let gs = gsnp4+ let cxt = cxtnp4++ -- Return.+ returnA -< (gs, cxt)+ where+ returnAWithoutMiddle = arr $ \(a, _b, c) -> (a, c)++-- | Step the debug free camera.+--+-- Check whether this mode is enabled before using this wire.+stepGameClockDebugFreeCamera :: Wire ImmutaballM (GameState, Double, IBStateContext) (GameState, IBStateContext)+stepGameClockDebugFreeCamera = proc (gsn, dt, cxtn) -> do+ let gsa = mkGameStateAnalysis cxtn gsn+ let netMov = gsa^.gsaNetRightForwardJump+ let (netRight, netForward, netJump) = netMov++ -- FRP architecture note: we have access to both 1) local state (FRP) and+ -- 2) the relatively global game state. If we wanted, we could use FRP to+ -- make a wire (time-varying value) that outputs the free camera position+ -- offset value, and just unconditionally set the game state, disregarding+ -- its input. We could also instead not use FRP but just calculate the+ -- next state.+ --+ -- To demonstrate capabilities, I instead take a third, hybrid approach:+ -- both. Use FRP to calculate a diff locally, and then apply that diff to+ -- the state (relatively) globally.++ let (unrotatedNetMovementVec :: Vec3 Double) = Vec3 (fromIntegral netRight) (fromIntegral netForward) (fromIntegral netJump)+ let (netMovementVec :: Vec3 Double) = tilt3ySimple (v3normalize $ (gsa^.gsaView.mviewTarget) `minusv3` (gsa^.gsaView.mviewPos)) `mv3` unrotatedNetMovementVec+ -- relativeNetMovementVec is not needed: netMovementVec _already_ includes the free camera rotation, not just the original orientation in the level file.+ --let (relativeNetMovementVec :: Vec3 Double) = aimVert3DSimple (Just $ 0.99*tau) (gsn^.gsDebugState.gdsCameraAimUpRadians) . aimHoriz3DSimple (gsn^.gsDebugState.gdsCameraAimRightRadians) $ netMovementVec+ let netMovementVec' = if' (not freeCameraRelative) unrotatedNetMovementVec $ netMovementVec+ posOffset <- integrate zv3 -< (dt * freeCameraSpeed) `sv3` netMovementVec'++ -- FRP (local).+ posOffsetDiff <- differentiate -< posOffset+ -- State (global).+ let posOffsetUpdate = gsDebugState.gdsCameraPosOffset %~ (+ posOffsetDiff)+ let gsnp1 = gsn & posOffsetUpdate++ -- Identity output.+ let gs = gsnp1+ let cxt = cxtn++ -- Return.+ returnA -< (gs, cxt)++ where+ -- Units per second.+ freeCameraSpeed :: Double+ freeCameraSpeed = 2.0++ -- Relative to aim? This is normal behavior, but temporarily setting+ -- this to False is an option.+ freeCameraRelative :: Bool+ freeCameraRelative = True++-- | Expend dt to step the ball through the physical world.+--+-- Apply gravity and handle collisions.+stepGameBallPhysics :: Wire ImmutaballM (GameState, Double, IBStateContext) (GameState, IBStateContext)+stepGameBallPhysics = proc (gsn, dt, cxtn) -> do+ let gsa = mkGameStateAnalysis cxtn gsn++ let x'cfg = cxtn^.ibContext.ibStaticConfig++ -- Find the gravity vector.+ let gravityVectorUnrotated = (gsa^.gsaUpVec) & z3 %~ negate -- Mirror on xy plane.+ let gravityVector = rotatexySimple (gsn^.gsCameraAngle) `mv3` gravityVectorUnrotated+ -- Don't apply the gravity vector yet, since we might apply too much dt,+ -- e.g. before the ball rests back on the floor; this could make the ball+ -- bouncy, especially depending on the frame rate.+ {-+ let gravityAcceleration = x'cfg^.x'cfgGravity+ let updateGravity =+ (gsBallVel %~ (+ (dt * gravityAcceleration) `sv3` gravityVector)) .+ id+ let gsnp1 = gsn & updateGravity+ -}+ let gsnp1 = gsn+ let cxtnp1 = cxtn++ -- Advance the ball through the physical world, handling collisions.+ --+ -- Expend dt to apply its velocity to the position, handling collisions by+ -- applying reflections.+ let (ballPos', ballVel') = physicsBallAdvance x'cfg (gsnp1^.gsSol) (gsnp1^.gsSolAnalysis.saPhysicsAnalysis) (gsnp1^.gsSolAnalysis.saOtherAnalysis) (gsnp1^.gsBallRadius) gravityVector gsnp1 dt (gsnp1^.gsBallPos) (gsnp1^.gsBallVel)+ let updateBall =+ (gsBallPos .~ ballPos') .+ (gsBallVel .~ ballVel') .+ id+ let (gsnp2, cxtnp2) = (gsnp1 & updateBall, cxtnp1)++ -- Identify output.+ let gs = gsnp2+ let cxt = cxtnp2++ -- Return.+ returnA -< (gs, cxt)++-- | Expend dt to step the ball through the physical world, handling collisions.+physicsBallAdvance :: StaticConfig -> LevelIB -> SolPhysicsAnalysis -> SolOtherAnalysis -> Double -> Vec3 Double -> GameState -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)+physicsBallAdvance = \x'cfg -> case (x'cfg^.x'cfgRequestAlternativePhysics) of+ Nothing -> physicsBallAdvanceDefault x'cfg+ Just "default" -> physicsBallAdvanceDefault x'cfg+ Just "stationary" -> physicsBallAdvanceStationary x'cfg+ Just "ghostly" -> physicsBallAdvanceGhostly x'cfg+ Just "brute-force" -> physicsBallAdvanceBruteForce x'cfg+ Just "bsp" -> physicsBallAdvanceBSP x'cfg+ _ -> physicsBallAdvanceDefault x'cfg++-- | Expend dt to step the ball through the physical world, handling collisions.+physicsBallAdvanceDefault :: StaticConfig -> LevelIB -> SolPhysicsAnalysis -> SolOtherAnalysis -> Double -> Vec3 Double -> GameState -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)+physicsBallAdvanceDefault x'cfg level spa soa ballRadius gravityVector gs dt ballPos ballVel = choice_ x'cfg level spa soa ballRadius gravityVector gs dt ballPos ballVel+ where+ choice_ :: StaticConfig -> LevelIB -> SolPhysicsAnalysis -> SolOtherAnalysis -> Double -> Vec3 Double -> GameState -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)+ --choice_ = physicsBallAdvanceStationary+ --choice_ = physicsBallAdvanceGhostly+ --choice_ = physicsBallAdvanceBruteForce+ choice_ = physicsBallAdvanceBSP++-- | For debugging or performance checking, keep the ball stationary.+physicsBallAdvanceStationary :: StaticConfig -> LevelIB -> SolPhysicsAnalysis -> SolOtherAnalysis -> Double -> Vec3 Double -> GameState -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)+physicsBallAdvanceStationary _x'cfg _level _spa _soa _ballRadius _gravityVector _gs _dt p0 v0 = (p1, v0)+ where+ p1 = p0++-- | For debugging or performance checking, ignore all collision checking.+physicsBallAdvanceGhostly :: StaticConfig -> LevelIB -> SolPhysicsAnalysis -> SolOtherAnalysis -> Double -> Vec3 Double -> GameState -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)+physicsBallAdvanceGhostly x'cfg _level _spa _soa _ballRadius gravityVector _gs dt p0 v0 = (p1, v1)+ where+ p1 = p0 + (dt `sv3` v1)+ v1 = v0 + ((dt * gravityAcceleration) `sv3` gravityVector)+ gravityAcceleration = x'cfg^.x'cfgGravity++-- | This version completely ignores the BSP. It checks collisions with every+-- lump every frame.+physicsBallAdvanceBruteForce :: StaticConfig -> LevelIB -> SolPhysicsAnalysis -> SolOtherAnalysis -> Double -> Vec3 Double -> GameState -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)+physicsBallAdvanceBruteForce = physicsBallAdvanceBruteForceCompute 0 0.0 0.0++-- | Finish computing 'physicsBallAdvanceBruteFroce'.+--+-- Make a line segment from p0 to p1, where p1 is p0 + dt*v, i.e. the tentative+-- end-point of the ball's position after the frame, if no collisions occur.+-- Check all lumps for a closest collision, and if one is found, expend enough+-- dt to advance the ball to the point of intersection, and then reflect v0+-- about the face's plane, and repeat, checking again for all lumps using the+-- remaining dt to expend. Since the path represent the center of the ball,+-- use the sphere's radius to check for distance. For edges and vertices, use+-- as the plane a constructed plane where the plane is orthogonal to the line+-- from the (new) ball center to the point of intersection. Once no collisions+-- have been detected, freely expend the remaining dt. We now have the new+-- ball position, at the last p1.+--+-- Parameters:+--+-- numCollisions:+-- for testing squishes, this is incremented each collision, and once it+-- exceeds the maximum, the squish condition is detected and the physics+-- engine will skip any remaining collisions, letting the ball go through lumps.+-- thresholdTimeRemaining:+-- Once this much dt has been expended, reset the 2 squish detection+-- parameters (this and numCollisions).+-- thresholdRDistanceRemaining:+-- Once the ball has traveled this much distance in terms of ball radiuses,+-- reset the squish state in this condition too. We don't want the ball the+-- ghost through walls if the frame is slow and processing a lot of dt at+-- once, simply because it made many collisions in a frame even though it+-- traveled a long distance (e.g. spinning around in a cone if the system+-- briefly pauses), so we have thresholdTimeRemaining; and we also have+-- distance because otherwise the ball can always go fast enough so that it+-- bounces enough times between lumps and ghosts through.+--+-- TODO: I once observed a rare glitching through a wall on Medium 16.+-- Probably make the planes and collisions handling a little more rigorous to+-- fix it, but the brute force algorithm isn't designed to be the primary+-- physics algorithm, so it probably isn't very important.+physicsBallAdvanceBruteForceCompute :: Integer -> Double -> Double -> StaticConfig -> LevelIB -> SolPhysicsAnalysis -> SolOtherAnalysis -> Double -> Vec3 Double -> GameState -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)+--physicsBallAdvanceBruteForceCompute numCollisions thresholdTimeRemaining thresholdRDistanceRemaining x'cfg level spa _soa ballRadius gravityVector gs dt p0 v0 =+physicsBallAdvanceBruteForceCompute numCollisions thresholdTimeRemaining thresholdRDistanceRemaining x'cfg level spa soa ballRadius gravityVector gs dt p0 v0+ -- Redundantly ensure dt is not zero or negative.+ | dt <= 0 + smallNum = let (p1, v1) = (p0, v0) in (p1, v1)+ -- Check the optional maxPhysicsStepTime config.+ | Just maxPhysicsStepTime <- x'cfg^.x'cfgMaxPhysicsStepTime, dt > maxPhysicsStepTime + smallNum =+ let (p1, v1) =+ physicsBallAdvanceBruteForceCompute+ numCollisions+ thresholdTimeRemaining+ thresholdRDistanceRemaining+ x'cfg+ level+ spa+ soa+ ballRadius+ gravityVector+ gs+ maxPhysicsStepTime+ p0+ v0 in+ physicsBallAdvanceBruteForceCompute+ numCollisions+ thresholdTimeRemaining+ thresholdRDistanceRemaining+ x'cfg+ level+ spa+ soa+ ballRadius+ gravityVector+ gs+ (dt - maxPhysicsStepTime)+ p1+ v1+ -- Now step the physics.+ | otherwise =+ case closestLumpIntersecting of -- Find the next collision.+ Nothing -> -- No more collisions this frame.+ -- Gravity: apply gravity to the rest of the path.+ let (p1, v1) = (p0 + (dt `sv3` v0), v0g dt) in (p1, v1) -- Expend the rest of dt after the last collision.+ Just (_lastLi', edt, p0', v0') -> -- Found the next collision. Expend ‘edt’ to advance the pall to p0'.+ -- Gravity: only apply the gravity vector through to the next collision, later on when we produce what is v0' here.+ let travelRDistance = (p0' - p0)^.r3 / ballRadius; trd = travelRDistance in+ physicsBallAdvanceBruteForceCompute+ ( if' resetSquishState 0 (numCollisions + 1) )+ ( if' resetSquishState (x'cfg^.x'cfgMaxFrameCollisionsDtThreshold - edt) (thresholdTimeRemaining - edt) )+ ( if' resetSquishState (x'cfg^.x'cfgMaxFrameCollisionsRDistanceThreshold - edt) (thresholdRDistanceRemaining - trd) )+ x'cfg+ level+ spa+ soa+ ballRadius+ gravityVector+ gs+ (dt - edt)+ p0'+ v0'++ where+ bounceReturn = x'cfg^.x'cfgBounceReturn+ gravityAcceleration = x'cfg^.x'cfgGravity++ resetSquishState+ | thresholdTimeRemaining <= 0 = True+ | thresholdRDistanceRemaining <= 0 = True+ | otherwise = False++ -- | Find the closest lump intersecting the ball's path, for collisions.+ closestLumpIntersectingRaw :: Maybe (Int32, Double, Vec3 Double, Vec3 Double)+ --closestLumpIntersectingRaw = safeHead . sortOn (^._2) . catMaybes . toList $ lumpsIntersecting+ closestLumpIntersectingRaw = safeHead . sortOn (^._2) . catMaybes $ lumpsIntersecting++ -- | Check cfgMaxFrameCollisions, and whether dt is already exhausted.+ closestLumpIntersecting :: Maybe (Int32, Double, Vec3 Double, Vec3 Double)+ closestLumpIntersecting+ | numCollisions >= (x'cfg^.x'cfgMaxFrameCollisions) = Nothing+ | dt <= 0 || (dt - 0) `equivalentSmall` 0 = Nothing+ | otherwise = closestLumpIntersectingRaw++ -- | Map each lump into the closest collision, and get 1) the dt+ -- needed to expend to advance the ball to this point of collision, 2)+ -- the new ball pos and 3) ball vel.+ --+ -- Only the closest collision will be used for advancing the ball,+ -- before checking for collision again afterwards.+ --lumpsIntersecting :: Array Int32 (Maybe (Double, Vec3 Double, Vec3 Double))+ --lumpsIntersecting = level^.solLv <&> \lump ->+ lumpsIntersecting :: [Maybe (Int32, Double, Vec3 Double, Vec3 Double)]+ lumpsIntersecting = flip fmap (zip [0..] (toList $ level^.solLv)) . uncurry $ \li lump ->+ let+ checkVertices :: Bool+ -- I suspect checking the length of facesIntersectingNoBounds+ -- can introduce a space leak (e.g. try out retour de force 2),+ -- but it turns out probably checking the length of+ -- edgesIntersecting is probably better anyway.+ --checkVertices | (_:_:_:_) <- facesIntersectingNoBounds = True | otherwise = False+ -- TODO FIXME: verify this also isn't broken, but for now just+ -- always check verts.+ --checkVertices | (_:_:_) <- edgesIntersecting = True | otherwise = False+ checkVertices = True+ verticesIntersecting :: [(Int32, Double, Vec3 Double, Vec3 Double)]+ verticesIntersecting = if' (not checkVertices) [] $ do+ -- For each verte,+ vi <- indirection <$> [lump^.lumpV0 .. lump^.lumpV0 + lump^.lumpVc - 1]+ let vertex = (level^.solVv) ! vi+ let v = vertex^.vertP++ -- Find the distance between the path (lp) and the vertex.+ let vd = abs $ line3PointDistance lp v++ -- Skip if the _infinite_ line extension from lp and+ -- the vertex are too far away. This allows an early check+ -- and also to ensure the call to line3DistanceCoordFromPoint is valid.+ guard $ vd <= ballRadius++ -- Find the closest point coord on lp to the vertex.+ let lpClosestX = line3PointCoord lp v++ -- Find how far away the candidates for x (where the ball+ -- is exactly ballRadius away from the vertex) are from+ -- closestX.+ let lpCoordOffset = line3DistanceCoordFromPoint lp v ballRadius++ -- Find the coord on lp where the distance is the ball's+ -- radius. Skip if we have nothing on lp.+ let xCandidates = [lpClosestX - lpCoordOffset, lpClosestX + lpCoordOffset]+ (x:_) <- return . sort . filter (\x -> 0 <= x + smallNum && x - smallNum <= 1) $ xCandidates++ -- Skip if x is not on lp. (Already done by the filter above.)+ --guard $ 0 <= x + smallNum && x - smallNum <= 1++ -- Find the ball intersection point.+ let ballIntersection = line3Lerp lp x `v3orWith` (lp^.p0l3)++ -- For the reflecting plane for the ball's velocity,+ -- construct a virtual plane essentially with the vector+ -- from the vertex to the ball's position at intersection+ -- as normal.+ let virtualPlane = normalPlane3 (v3normalize $ ballIntersection - v) 0++ -- Make sure the ball is going towards the vertex, not away+ -- from it, similar to the normal check for planes for+ -- avoiding multiple collisions in a single step for me+ -- thasme lump.+ guard $ (lp^.a0l3) `d3` (virtualPlane^.abcp3) <= 0++ -- Return the results of this collision, which is used if+ -- it is found to be the first potential collision on the+ -- path lp.+ let edt = x * dt+ let p0' = ballIntersection+ let v0' = plane3ReflectPointAmount (virtualPlane & dp3 .~ 0) (v0g edt) (bounceReturn) -- v0g: Apply gravity for this path.+ return $ (li, edt, p0', v0')++ checkEdges :: Bool+ -- TODO FIXME: length >= 2 seems to cause edge collision detection+ -- to not work. For now just disable this check.+ --checkEdges | (_:_:_) <- facesIntersectingNoBounds = True | otherwise = False+ checkEdges = True+ edgesIntersecting :: [(Int32, Double, Vec3 Double, Vec3 Double)]+ edgesIntersecting = if' (not checkEdges) [] $ do+ -- For each edge,+ ei <- indirection <$> [lump^.lumpE0 .. lump^.lumpE0 + lump^.lumpEc - 1]+ let edge = (level^.solEv) ! ei+ let (vi, vj) = (((level^.solVv) ! (edge^.edgeVi))^.vertP, ((level^.solVv) ! (edge^.edgeVj))^.vertP)+ let el = line3Points vi vj++ -- Find the distance between the path (lp) and the edge.+ let ed = abs $ line3Line3Distance lp el++ -- Skip if _the infinite lines_ are too far away. However,+ -- the line segments may still be too far away even if the+ -- infinite lines are not, so we'll filter lp coords in [0, 1].+ guard $ ed <= ballRadius++ -- Find the closest point.+ --Just (lpx, elx) <- return $ line3Line3ClosestCoords lp el+ let (lpx, elx) = line3Line3ClosestCoordsAny lp el+ let elv = line3Lerp el elx++ -- Find the point on lp where the ball is at when it first+ -- collides with the edge.+ let lpCoordOffset = abs $ line3DistanceCoordFromPoint lp elv ballRadius++ -- Find candidates for x: they must be within [0, 1]; if no+ -- candidates pass, the line segment (the path) is too far+ -- away from the edge, so we'll skip this edge.+ let xCandidates = [lpx - lpCoordOffset, lpx + lpCoordOffset]+ (x:_) <- return . sort . filter (\x -> 0 <= x + smallNum && x - smallNum <= 1) $ xCandidates++ let ballIntersection = line3Lerp lp x `v3orWith` (lp^.p0l3)++ let edgePointBallIntersection = line3Lerp el . line3PointCoord el $ ballIntersection++ -- Check to make sure the intersection is actually on the+ -- edge _line segment_, not to leave it checking its+ -- infinite line only!+ let intersectionElx = line3PointCoord el edgePointBallIntersection+ guard $ 0 <= intersectionElx + smallNum && intersectionElx - smallNum <= 1++ -- For the reflecting plane for the ball's velocity,+ -- construct a virtual plane essentially with the vector+ -- from the edge to the ball's position at intersection as+ -- normal.+ let virtualPlane = normalPlane3 (v3normalize $ ballIntersection - edgePointBallIntersection) 0++ -- Make sure the ball is going towards the edge, not away+ -- from it, similar to the normal check for planes for+ -- avoiding multiple collisions in a single step for the+ -- same lump.+ guard $ (lp^.a0l3) `d3` (virtualPlane^.abcp3) <= 0++ -- Return the results of this collision, which is used if+ -- it is found to be the first potential collision on the+ -- path lp.+ let edt = x * dt+ let p0' = ballIntersection+ let v0' = plane3ReflectPointAmount (virtualPlane & dp3 .~ 0) (v0g edt) (bounceReturn) -- v0g: Apply gravity for this path.+ return $ (li, edt, p0', v0')++ -- | Find all faces that intersect p0->p1.+ --+ -- Find where the p0->p1 intersects a side's plane. However,+ -- handle the bounds of the face: if the face's plane's point+ -- of intersection with p0->p1 is not behind (<=) all other+ -- planes, then it's beyond the edges bordering the side.+ --+ -- Note this requires that all sides have a normal pointing+ -- _away_ from the convex lump inside the level file.+ facesIntersecting :: [(Int32, Double, Vec3 Double, Vec3 Double)]+ facesIntersecting = facesIntersecting' False+ _facesIntersectingNoBounds :: [(Int32, Double, Vec3 Double, Vec3 Double)]+ _facesIntersectingNoBounds = facesIntersecting' True+ -- | We use ignoreEdgeBounds when checking for edge and vertex+ -- intersections.+ facesIntersecting' :: Bool -> [(Int32, Double, Vec3 Double, Vec3 Double)]+ facesIntersecting' ignoreEdgeBounds = do+ let errMsg = "Internal error: physicsBallAdvanceBruteForceCompute: sides data missing for lump with li " ++ (show li) ++ "."+ let sidePlanes = flip M.lookup (spa^.spaLumpPlanes) li `morElse` error errMsg++ -- For each side (check useDirectSol for which set of sides to use),+ (sidx, sidePlane) <-+ if useDirectSol+ then do+ -- For each side,+ si <- indirection <$> [lump^.lumpS0 .. lump^.lumpS0 + lump^.lumpSc - 1]+ let side = (level^.solSv) ! si+ -- Get its plane.+ let sidePlane = normalPlane3 (side^.sideN) (side^.sideD)+ return (si, sidePlane)+ else do+ (sidx, sidePlane) <- zip [0..] sidePlanes+ return (sidx, sidePlane)+ -- Find where on lp it intersects; abort this try if it doesn't.+ Just x <- return $ line3CoordAtDistancePlane3 sidePlane lp ballRadius+ -- Only consider intersections on the line segment.+ --guard $ (0 <= x + smallNum && x - smallNum <= 1)+ -- The right side prevents the ball from ghost-glitching+ -- through a wall for very short 'lp' ('onPlaneCheck').+ let intersectsLp = 0 <= x + smallNum && x - smallNum <= 1+ let onPlaneCheck = plane3PointDistance sidePlane (lp^.p0l3) `near` ballRadius && plane3PointDistance sidePlane (lp^.p1l3) `near` ballRadius+ guard $ intersectsLp || onPlaneCheck++ -- Get the point in 3D space: this is where the ball would+ -- be if advanced to this intersection.+ let ballIntersection = line3Lerp lp x `v3orWith` (lp^.p0l3)+ -- Get the point on the plane where this collision would+ -- take place: the closest point on the plane to+ -- 'ballIntersection'.+ let planeIntersection = pointToPlane ballIntersection sidePlane+ -- Only consider intersections whose plane intersection+ -- points are behind all other sides.+ guard . (if' ignoreEdgeBounds (const True) id) . and $ do+ if useDirectSol+ then do+ let si = sidx++ -- For every other side …+ sj <- [lump^.lumpS0 .. lump^.lumpS0 + lump^.lumpSc - 1]+ guard $ sj /= si+ let sidej = (level^.solSv) ! sj+ let sidejPlane = normalPlane3 (sidej^.sideN) (sidej^.sideD)++ -- Make sure the point is behind this plane.+ return $ plane3PointDistance sidejPlane planeIntersection <= 0+ else do+ -- For every other side …+ (sjidx, sidejPlane) <- zip [0..] sidePlanes+ guard $ sjidx /= sidx++ -- Make sure the point is behind this plane.+ return $ plane3PointDistance sidejPlane planeIntersection <= 0++ -- Finally, make sure we only register a collision if the+ -- ball is going towards this plane, not away from it: e.g.+ -- you can only collide against a top face from above, not+ -- from underneath. This prevents the same lump causing+ -- multiple collision events for what should be a single+ -- collision. We can do this by making sure the dot+ -- product of the direction (axis) of ‘lp’ and the plane+ -- normal is not positive.+ guard $ (lp^.a0l3) `d3` (sidePlane^.abcp3) <= 0++ -- We've found an intersection. Now calculate the values+ -- we would need if we ended up picking this after finding it+ -- is indeed the closest.++ let edt = x * dt+ let p0' = ballIntersection+ let v0' = plane3ReflectPointAmount (sidePlane & dp3 .~ 0) (v0g edt) (bounceReturn) -- v0g: Apply gravity for this path.+ return $ (li, edt, p0', v0')++ where+ useDirectSol = False++ -- | Lower dimensionalities appear before higher+ -- dimensionalities for equal distances. 'sortOn' is a stable+ -- sort, so we can use it.+ allIntersecting = concat $+ [+ verticesIntersecting,+ edgesIntersecting,+ facesIntersecting+ ]++ sortedIntersecting = sortOn (^._2) $ allIntersecting++ lumpComponentIntersecting :: Maybe (Int32, Double, Vec3 Double, Vec3 Double)+ lumpComponentIntersecting = safeHead sortedIntersecting++ -- Make sure the lump isn't the one we last used for collisions.+ r = lumpComponentIntersecting+ in+ r++ where+ -- | Draw a line segment from the ball's position (center of+ -- sphere) to the tentative end-point if all dt were to be+ -- expended now using its current velocity.+ --+ -- TODO: double check edge cases of small lp don't cause+ -- issues. The guard has a p0 and p1 nearness check, but there+ -- might still be issues.+ lp = let p1 = p0 + (dt `sv3` v0) in line3Points p0 p1++ -- | It seems sols have indirection for lump sides, vertices, and+ -- edges, but not edge indices to vertices.+ indirection :: Int32 -> Int32+ indirection idx = (level^.solIv) ! idx++ v0g edt = v0 + ((edt * gravityAcceleration) `sv3` gravityVector) -- What velocity is if v0 has time added to gravity.++-- | Advance the ball's position and velocity, using the level BSP to handle+-- collisions and gravity.+--+-- Try expending the remaining dt, and detecting if any collisions are made+-- with any lumps or BSP partitions (e.g. check if the side (-1,0,1) of a plane+-- the position point is on would change). If there's a collision, then p0->p1+-- intersects, and this algorithm sorts all the detected intersections to only+-- use the closest one, and then also checks among the remaining collisions the+-- ones that are near by the closest ones, to make sure these possible+-- collisions aren't skipped if advancing to the closest collision also changes+-- e.g. which side of a plane the ball would be on if it is advanced to the+-- closest collision. We expend dt to advance the ball to the closest+-- collision and also check immediate (or near) new collisions the same+-- distance away from the closest collision. Then we repeat the process anew+-- with the remaining dt to expend on advancing the ball, until there are no+-- more collisions, in which case we expend the remaining dt.+--+-- Also add a squish check mechanism: only so many collisions are permitted+-- within a specific distance and for a specific amount of dt. If a squish+-- condition is detected, then instead of potentially looping forever (and+-- hanging), just abort collisions and let the ball ‘wall-glitch’ through walls+-- to move through them.+--+-- TODO: support moving bodies, since this currently only checks current static+-- position.+--+-- TODO: this is complete for stationary bodies but currently has a few bugs:+-- 1) It misses some edge collisions especially if the ball is resting and+-- falls off an edge (but can handle it correctly if they ball falls from a+-- distance onto the edge). (See commit log for more notes on some debugging+-- related to this.) To reproduce, make a simple single lump map with the+-- ball falling down onto it. Sometimes even falling it misses the edge+-- collision, but it always seems to fall through too early if the ball+-- rests first.+-- 2) FIXED: don't forget ballRadius when discarding BSP children.+-- 3) On Easy 3 (coins.sol), when the ball goes across an edge between the+-- lumps it starts on and in front of it, it can freeze for a second or two,+-- sometimes throwing the ball sort of randomly once it resumes.+-- 4) FIXED: ball no longer ghosts through certain lumps; the fix was the lump+-- plane construction deduplication threshold was too tight.+physicsBallAdvanceBSP :: StaticConfig -> LevelIB -> SolPhysicsAnalysis -> SolOtherAnalysis -> Double -> Vec3 Double -> GameState -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)+physicsBallAdvanceBSP x'cfg level spa soa ballRadius gravityVector gs dt p0 v0+ | Just maxPhysicsStepTime <- x'cfg^.x'cfgMaxPhysicsStepTime, dt > maxPhysicsStepTime =+ let (p1, v1) = advance 0 0.0 0.0 maxPhysicsStepTime p0 v0+ in (physicsBallAdvanceBSP x'cfg level spa soa ballRadius gravityVector gs) (dt - maxPhysicsStepTime) p1 v1+ | otherwise =+ advance 0 0.0 0.0 dt p0 v0++ where+ bounceReturn = x'cfg^.x'cfgBounceReturn+ gravityAcceleration = x'cfg^.x'cfgGravity++ -- | It seems sols have indirection for lump sides, vertices, and+ -- edges, but not edge indices to vertices.+ indirection :: Int32 -> Int32+ indirection idx = (level^.solIv) ! idx++ -- Descend each BSP to find the closest intersecting lump, collecting+ -- all other lumps within threshold distance from the closest+ -- insersection and then checking them (avoiding a wall-glitch) before+ -- proceeding.+ --+ -- Repeat until all remaining dt is expended, or a squish condition is+ -- detected.+ advance :: Integer -> Double -> Double -> Double -> Vec3 Double -> Vec3 Double -> (Vec3 Double, Vec3 Double)+ advance numLocalCollisions localDtExpended localDistance dtn pn vn+ | localDtExpended >= (x'cfg^.x'cfgMaxFrameCollisionsDtThreshold) = advance 0 0.0 0.0 dtn pn vn+ | localDistance >= (x'cfg^.x'cfgMaxFrameCollisionsRDistanceThreshold) = advance 0 0.0 0.0 dtn pn vn+ | numLocalCollisions >= (x'cfg^.x'cfgMaxFrameCollisions) = (p1, v1)+ | dt <= 0 = (p1, v1)+ | otherwise = result+ where+ -- | Tentative end-point: advance here if no collisions.+ p1 :: Vec3 Double+ p1 = pn + (dtn `sv3` vn)++ -- | Tentative end-point velocity: used if no collisions.+ v1 :: Vec3 Double+ v1 = vng dtn++ -- | What velocity is if vn has time added to gravity.+ vng edt = vg vn edt+ vg v edt = v + ((edt * gravityAcceleration) `sv3` gravityVector)++ -- | Draw a line segment from the ball's position (center of+ -- sphere) to the tentative end-point if all dt were to be+ -- expended now using its current velocity.+ _lp :: Line3 Double+ _lp = line3Points p0 p1++ -- | Take a position/point, and the given body by index, and+ -- convert the position/coords to be in terms of the body frame+ -- of reference, so that you can use vertices in the body that+ -- could be moving on a path directly. Return to world coords+ -- with 'ipb' afterwards.+ pb :: Vec3 Double -> Int32 -> Vec3 Double+ pb p bi =+ let bodyTranslation = getBodyTranslation level soa gs bi 0 in+ p - bodyTranslation++ ipb :: Vec3 Double -> Int32 -> Vec3 Double+ ipb p bi =+ let bodyTranslation = getBodyTranslation level soa gs bi 0 in+ p + bodyTranslation++ -- | Lump, collection of lumps to check, distance to closest,+ -- time required to expend to closest.+ nextCollision :: Maybe NextCollision+ nextCollision =+ let addNothing (a, b) = (a, b, Nothing) in+ (^.ncsNc) . flip execState (NextCollisionState Nothing ((addNothing . second ((^.lumpBSP)) <$>) . M.toList $ spa^.spaBodyBSPs)) . fix $ \me -> do+ bspsLeft <- gets (^.ncsBspsLeft)+ case bspsLeft of+ [] -> return ()+ ((bi, bsp, mbspParent):bspsLeft') -> do+ modify (ncsBspsLeft .~ bspsLeft')++ let p0' = pb p0 bi+ let p1' = pb p1 bi+ let lp' = line3Points p0' p1'++ -- We now have a current best distance. If this+ -- BSP partition, including all lumps directly on+ -- it, is entirely further away, we can skip it!+ mcurrentBestDistance <- gets (((^.ncDistanceTo) <$>) . (^.ncsNc))+ let mdistanceToParentPlane = do+ bspParent <- mbspParent+ let parentPlane = bspParent^.lbspppParent.lbsppPlane+ _currentBestDistance <- mcurrentBestDistance+ if' ((lp'^.a0l3.r3) <= smallishInfiniteLineThreshold)+ (return $ plane3PointDistance parentPlane p0')+ $ do+ distanceInFront <- (/ (lp'^.a0l3.r3)) <$> line3CoordAtDistancePlane3 parentPlane lp' ballRadius+ distanceBehind <- (/ (lp'^.a0l3.r3)) <$> line3CoordAtDistancePlane3 parentPlane lp' (-ballRadius)+ return $ min distanceInFront distanceBehind+ let canSkipThisBSP = (== Just True) $ do+ currentBestDistance <- mcurrentBestDistance+ distanceToParentPlane <- mdistanceToParentPlane+ return $ distanceToParentPlane > currentBestDistance+ if' canSkipThisBSP me $ do++ let withEmpty _then = do+ me+ let withLeaf bspPartition then_ = do+ then_ bspPartition+ let withFork l bspPartition r then_ = do+ -- Try to eliminate l and r if we can show+ -- there won't be a collision on that side of+ -- the partition, then queue them up for+ -- processing.+ let queueL =+ (plane3PointDistance (bspPartition^.lbsppPlane) p0') - ballRadius <= 0 ||+ (plane3PointDistance (bspPartition^.lbsppPlane) p1') - ballRadius <= 0+ let queueR =+ (plane3PointDistance (bspPartition^.lbsppPlane) p0') + ballRadius >= 0 ||+ (plane3PointDistance (bspPartition^.lbsppPlane) p1') + ballRadius >= 0+ let newParent isRightBranch = LumpBSPPartitionParent {+ _lbspppParent = bspPartition,+ _lbspppIsRightBranch = isRightBranch+ }+ when queueL $ do+ modify (ncsBspsLeft %~ ((bi, l, Just $ newParent False):))+ when queueR $ do+ modify (ncsBspsLeft %~ ((bi, r, Just $ newParent True):))+ then_ bspPartition+ deconsLabeledBinTree withEmpty withLeaf withFork bsp $ \bspPartition -> do+ -- Check each lump directly intersecting this+ -- BSP partition for a possible collision.+ let (lumpsi :: S.Set Int32) = bspPartition^.lbsppLumps+ (>> me) . forM_ lumpsi $ \lumpi -> do+ let lump = (level^.solLv) ! lumpi+ -- For now, we don't know all the details+ -- of the collision while we collect the+ -- closest and candidate other close lumps.+ -- Check each plane for a collision, and+ -- take the closest. We detect a collision+ -- when lp' intersects with a lump's plane+ -- at a point behind all the other planes.++ -- Skip this lump if it's detail flag is+ -- set, which indicates it's only for+ -- rendering and not part of the physical+ -- world.+ if' (((lump^.lumpFl) .&. lumpFlagDetail) /= 0) (return ()) $ do++ -- | Assume sorted and non-empty lists+ let unsafeHead xs = case xs of (x:_) -> x; _ -> error "Internal error: physicsBallAdvanceBSP: unsafeHead called on empty list."+ let nclDist ncl = unsafeHead (ncl^.nclLpPlaneIntersections) ^. lpxc.llpciDistance+ let nclPrio ncl = unsafeHead (ncl^.nclLpPlaneIntersections) ^. lpxc.llpciPriority+ -- | Discard all collisions not close to the nearest.+ -- Also discard all collisions that diminish+ -- priority within the same lump.+ let (filterChecks :: NextCollisionLump -> [NextCollisionLump] -> [NextCollisionLump]) = \best checks ->+ flip filter checks $ \candidate ->+ -- Make sure it's close enough to best.+ (nclDist candidate) - (nclDist best) <= smallishNum &&+ -- Make sure it's not priority+ -- diminishing (increasing) within the+ -- same lump, unless the distance+ -- is significantly better.+ (not+ ((candidate^.nclLumpi) == (best^.nclLumpi)) &&+ (nclPrio candidate > nclPrio best) &&+ (nclDist candidate - smallishNum >= nclDist best)+ )+ -- | Get this lump's planes.+ let (lumpPlanes :: [Plane3 Double]) = M.lookup lumpi (spa^.spaLumpPlanes) `morElse`+ (error $ "Internal error: physicsBallAdvanceBSP: nextCollision: failed to find planes for lump " ++ show lumpi ++ ".")+ -- Query the current best and checks.+ mnc <- gets (^.ncsNc)+ let mncBest = (^.ncClosestLump) <$> mnc+ let mncChecks = (^.ncCheckLumps) <$> mnc+ let _mncFilterChecks = (pure filterChecks <*> mncBest) `morElse` id+ -- | Get this plane's face intersections with lp'.+ -- Also add a state layer that performs a+ -- simple preliminary test to see if we need+ -- to check for edge and vertex collisions.+ let (lpFaceIntersectionsRaw :: [LumpLpPlaneFaceIntersection], needCheckEdgeVertCol :: Bool) =+ flip runState False . runListT $ do+ -- Get each plane and a list of all+ -- other planes for this lump.+ (planeIdx, (plane, others)) <- liftList $ zip [0..] $ listOthers lumpPlanes++ {-+ -- Make sure we only register a collision if the+ -- ball is going towards this plane, not away from it: e.g.+ -- you can only collide against a top face from above, not+ -- from underneath. This prevents the same lump causing+ -- multiple collision events for what should be a single+ -- collision. We can do this by making sure the dot+ -- product of the direction (axis) of ‘lp’ and the plane+ -- normal is not positive.+ guard $ (lp'^.a0l3) `d3` (plane^.abcp3) <= 0+ -}++ -- To register a collision, make sure+ -- the advancement would bring the ball+ -- behind the plane: either p0 is+ -- outside (plane distance > 1) going+ -- in (== 0 || == 1), or p0 is+ -- strictly intersecting (== 0) going+ -- in (== -1).+ -- Note: for edges and+ -- vertices, there is more than 1 plane+ -- attached to the edge, and in this+ -- case lp' needs to be going+ -- not-outside the plane for at least 1+ -- of the attach planes, not all. But+ -- since we check each plane, for an+ -- edge collision, there is still at+ -- least 1 plane that will pass this+ -- test, so we don't need to modify it for+ -- edges and vertices.+ guard $ signum (plane3PointDistance plane p1' - ballRadius - smallishNum) < signum (plane3PointDistance plane p0' - ballRadius + smallishNum)++ -- See if we need to use special logic+ -- for very small steps. If so, we'll+ -- focus more on just p0' without p1'.+ let approximatelyAPoint = (lp'^.a0l3.r3) <= smallishInfiniteLineThreshold++ -- Find the coord on lp' at which we+ -- find the intersection.+ let intersectionCoord+ | approximatelyAPoint = 0.0+ | otherwise = line3CoordAtDistancePlane3 plane lp' ballRadius `morElse` 0.0+ let intersectionBallPoint = line3Lerp lp' intersectionCoord+ let intersectionPoint+ | approximatelyAPoint = pointToPlane p0' plane+ | otherwise = line3Lerp lp' $ intersectionCoord + ballRadius/(lp'^.a0l3.r3)+ -- Find the distance from p0' across lp'+ -- (or shortest line) to the plane+ -- (where with ballRadius there is an+ -- intersection). i.e. distance ball+ -- would travel to reach the collision.+ let distance+ | approximatelyAPoint = plane3PointDistance plane p0'+ | otherwise = (intersectionBallPoint - p0')^.r3+ -- Find the time to reach intersectionCoord.+ let timeTo = (dtn * intersectionCoord) `florWith` 0.0++ -- Now, make sure the intersection+ -- point is on the lump, and not+ -- outside the lump on some arbitrary+ -- plane, since we have more data to+ -- work with now. Also we do our+ -- preliminary check for edge and+ -- vertex collision detection here.+ let (behindOthers, needCheck) = split $+ [+ r |+ other <- others,+ let d = plane3PointDistance other intersectionPoint,+ let behind = d <= 0,+ -- Also do preliminary+ -- check for vertex and edge+ -- collision.+ let medgeLine = plane3Plane3 plane other,+ let+ foundNeedCheck Nothing = False+ foundNeedCheck (Just edgeLine)+ | behind = False+ | line3PointDistance edgeLine p0' <= ballRadius = True+ | line3PointDistance edgeLine p1' <= ballRadius = True+ -- Note: line3Line3Distance checks the _infinite_+ -- line distance, so we still need to test more+ -- precisely, but we at least shouldn't miss+ -- edge or vert collisions.+ | line3Line3Distance edgeLine lp' <= ballRadius = True+ | otherwise = False,+ let r = (behind, foundNeedCheck medgeLine)+ ]+ -- See if we need to check for+ -- edges and vertices.+ let behindAllOthers = and behindOthers+ when (not behindAllOthers) $ do+ alreadyTested <- lift $ get+ when (not alreadyTested) $ do+ let anyFoundNeedCheck = or needCheck+ when anyFoundNeedCheck $ do+ lift $ put True+ -- Now skip if we're not behind all others.+ -- (We should skip on edge and+ -- vertex collisions if I'm not+ -- mistaken, but in such cases+ -- we'll still check later for edge+ -- and vertex collisions.+ guard $ behindAllOthers++ -- Construct the result.+ return . fix $ \_lpLumpX -> LumpLpPlaneFaceIntersection {+ _llpfiPlaneIdx = planeIdx,+ _llpfiPlane = plane,+ _llpfiDistance = distance,+ _llpfiTimeTo = timeTo,++ _llpfiLi = lumpi,+ _llpfiBi = bi,+ _llpfiBodyTranslation = getBodyTranslation level soa gs bi 0,++ _llpfiLp = lp',+ _llpfiIntersection = intersectionPoint,++ _llpfiIntersectionLx = intersectionCoord,+ _llpfiIntersectionBall = intersectionBallPoint+ }+ -- Quick test: skip if no face+ -- intersections and no need to check for+ -- edge or vertex collisions.+ if' (null lpFaceIntersectionsRaw && not needCheckEdgeVertCol) (return ()) $ do++ -- Check for edge and vertex collisions.+ let (lpEdgeVertIntersectionsRaw :: [LumpLpPlaneIntersection])+ | not needCheckEdgeVertCol = []+ | otherwise =+ let (edgeIntersections :: [LumpLpPlaneEdgeIntersection]) = do+ -- For each edge on the lump,+ ei <- indirection <$> [lump^.lumpE0 .. lump^.lumpE0 + lump^.lumpEc - 1]+ let edge = (level^.solEv) ! ei+ let (vi, vj) = (((level^.solVv) ! (edge^.edgeVi))^.vertP, ((level^.solVv) ! (edge^.edgeVj))^.vertP)+ let el = line3Points vi vj++ -- Find the distance between the path (lp) and the edge.+ let ed = abs $ line3Line3Distance lp' el++ -- Skip if _the infinite lines_ are too far away. However,+ -- the line segments may still be too far away even if the+ -- infinite lines are not, so we'll filter lp' coords in [0, 1].+ guard $ ed <= ballRadius++ -- Find the closest point.+ --Just (lpx, elx) <- return $ line3Line3ClosestCoords lp el+ let (lpx, elx) = line3Line3ClosestCoordsAny lp' el+ let elv = line3Lerp el elx++ -- Find the point on lp where the ball is at when it first+ -- collides with the edge.+ let lpCoordOffset = abs $ line3DistanceCoordFromPoint lp' elv ballRadius++ -- Find candidates for x: they must be within [0, 1]; if no+ -- candidates pass, the line segment (the path) is too far+ -- away from the edge, so we'll skip this edge.+ let xCandidates = [lpx - lpCoordOffset, lpx + lpCoordOffset]+ (x:_) <- return . sort . filter (\x -> 0 <= x + smallNum && x - smallNum <= 1) $ xCandidates++ let ballIntersection = line3Lerp lp' x `v3orWith` (lp'^.p0l3)++ let edgePointBallIntersection = line3Lerp el . line3PointCoord el $ ballIntersection++ -- Check to make sure the intersection is actually on the+ -- edge _line segment_, not to leave it checking its+ -- infinite line only!+ let intersectionElx = line3PointCoord el edgePointBallIntersection+ guard $ 0 <= intersectionElx + smallNum && intersectionElx - smallNum <= 1++ -- For the reflecting plane for the ball's velocity,+ -- construct a virtual plane essentially with the vector+ -- from the edge to the ball's position at intersection as+ -- normal.+ let virtualPlane = normalPlane3 (v3normalize $ ballIntersection - edgePointBallIntersection) 0++ -- Make sure the ball is going towards the edge, not away+ -- from it, similar to the normal check for planes for+ -- avoiding multiple collisions in a single step for the+ -- same lump.+ guard $ (lp'^.a0l3) `d3` (virtualPlane^.abcp3) <= 0++ -- Obtain some preliminary+ -- results of this collision,+ -- which is used if it is found+ -- to be the first potential+ -- collision on the path lp'.+ let colEdt = x * dt+ let distance = (ballIntersection - p0')^.r3++ -- Return the result of this+ -- collision candidate.+ return $ LumpLpPlaneEdgeIntersection {+ _llpeiDistance = distance,+ _llpeiTimeTo = colEdt,++ _llpeiLi = lumpi,+ _llpeiBi = bi,+ _llpeiBodyTranslation = getBodyTranslation level soa gs bi 0,++ _llpeiLp = lp',+ _llpeiIntersection = edgePointBallIntersection,++ _llpeiIntersectionLx = x,+ _llpeiIntersectionBall = ballIntersection,++ _llpeiVirtualPlane = virtualPlane+ } in+ let (vertIntersections :: [LumpLpPlaneVertIntersection]) = do+ -- For each vertex,+ vi <- indirection <$> [lump^.lumpV0 .. lump^.lumpV0 + lump^.lumpVc - 1]+ let vertex = (level^.solVv) ! vi+ let v = vertex^.vertP++ -- Find the distance between the path (lp) and the vertex.+ let vd = abs $ line3PointDistance lp' v++ -- Skip if the _infinite_ line extension from lp and+ -- the vertex are too far away. This allows an early check+ -- and also to ensure the call to line3DistanceCoordFromPoint is valid.+ guard $ vd <= ballRadius++ -- Find the closest point coord on lp to the vertex.+ let lpClosestX = line3PointCoord lp' v++ -- Find how far away the candidates for x (where the ball+ -- is exactly ballRadius away from the vertex) are from+ -- closestX.+ let lpCoordOffset = line3DistanceCoordFromPoint lp' v ballRadius++ -- Find the coord on lp where the distance is the ball's+ -- radius. Skip if we have nothing on lp.+ let xCandidates = [lpClosestX - lpCoordOffset, lpClosestX + lpCoordOffset]+ (x:_) <- return . sort . filter (\x -> 0 <= x + smallNum && x - smallNum <= 1) $ xCandidates++ -- Skip if x is not on lp. (Already done by the filter above.)+ --guard $ 0 <= x + smallNum && x - smallNum <= 1++ -- Find the ball intersection point.+ let ballIntersection = line3Lerp lp' x `v3orWith` (lp'^.p0l3)++ -- For the reflecting plane for the ball's velocity,+ -- construct a virtual plane essentially with the vector+ -- from the vertex to the ball's position at intersection+ -- as normal.+ let virtualPlane = normalPlane3 (v3normalize $ ballIntersection - v) 0++ -- Make sure the ball is going towards the vertex, not away+ -- from it, similar to the normal check for planes for+ -- avoiding multiple collisions in a single step for me+ -- thasme lump.+ guard $ (lp'^.a0l3) `d3` (virtualPlane^.abcp3) <= 0++ -- Obtain some preliminary results of this collision,+ -- which is used if it is found to be the first potential+ -- collision on the path lp'.+ let colEdt = x * dt+ let distance = (ballIntersection - p0')^.r3++ -- Return the result of this collision candidate.+ return $ LumpLpPlaneVertIntersection {+ _llpviDistance = distance,+ _llpviTimeTo = colEdt,++ _llpviLi = lumpi,+ _llpviBi = bi,+ _llpviBodyTranslation = getBodyTranslation level soa gs bi 0,++ _llpviLp = lp',+ _llpviIntersection = v,++ _llpviIntersectionLx = x,+ _llpviIntersectionBall = ballIntersection,++ _llpviVirtualPlane = virtualPlane+ } in+ fmap VertIntersection vertIntersections ++ fmap EdgeIntersection edgeIntersections++ -- Aggregate all collisions.+ let lpIntersectionsRaw = fmap FaceIntersection lpFaceIntersectionsRaw ++ lpEdgeVertIntersectionsRaw++ -- Sort all collisions.+ let lpIntersectionsUnfiltered = sortBy lpxcomp lpIntersectionsRaw++ -- | If there are any intersections, see if+ -- the best intersection is better than our+ -- current best. (Otherwise we're done+ -- with this lump.)+ if' (null lpIntersectionsUnfiltered) (return ()) $ do++ -- We found at least one intersection.++ -- We'll only need the closest intersection on+ -- this lump, and intersections close+ -- enough within a smallishNum threshold to+ -- test for edge and vertex collisions+ -- later.+ let lpIntersectionsFiltered = case lpIntersectionsUnfiltered of+ [] -> [] -- (Should never happen.)+ (closest:rest) -> closest :+ let closestDist = closest^.lpxc.llpciDistance in+ filter (\inters -> (inters^.lpxc.llpciDistance) - closestDist <= smallishNum) rest+ let lpIntersections = lpIntersectionsFiltered++ -- Get the new best and new checks.+ let (lumpNc :: NextCollisionLump) = NextCollisionLump lumpi lpIntersections+ let (newBest, newChecksRaw)+ | Just oldBest <- mncBest, Just oldChecks <- mncChecks =+ if' (lumpNc `nclcomp` oldBest == LT) (lumpNc, oldBest:oldChecks) (oldBest, lumpNc:oldChecks)+ | otherwise = (lumpNc, [])+ let newChecks = filterChecks newBest newChecksRaw++ -- | Update the next collision state.+ modify $ ncsNc .~ Just NextCollision {+ _ncClosestLump = newBest,+ _ncCheckLumps = newChecks,+ _ncDistanceTo = nclDist newBest,+ _ncTimeTo = unsafeHead (newBest^.nclLpPlaneIntersections) ^. lpxc.llpciTimeTo+ }++ -- Now handle nextCollision and check checkLumps.++ -- | After the next collision, find (pn, vn, edt), position,+ -- velocity, and dt expended. After we handle this+ -- collision step, then we apply gravity based on the time+ -- expended to travel this distance, and continue looping+ -- through advance until we exhaust all collisions or reach a+ -- squish condition.+ afterNextCollision :: Maybe (Vec3 Double, Vec3 Double, Double)+ afterNextCollision = (<$> nextCollision) $ \nc ->+ let ourFlatten ((a, b), c) = (a, b, c) in+ ourFlatten (foldl' step (pn, vn) $ (nc^.ncClosestLump) : (nc^.ncCheckLumps), nc^.ncTimeTo)+ where+ step :: (Vec3 Double, Vec3 Double) -> NextCollisionLump -> (Vec3 Double, Vec3 Double)+ step (p, v) ncl+ -- 3 possible collision types: vertex, edge, or+ -- plane. The type depends on how many+ -- intersections there are within threshold+ -- distance to the closest. If there's only one+ -- planar intersection, it's a plane; if 2, it's an+ -- edge; if 3 or more, it's a vertex.+ | [] <- ncl^.nclLpPlaneIntersections =+ -- Redundant; internally should never happen.+ (p, v)+ -- | ((FaceIntersection px):[]) <- ncl^.nclLpPlaneIntersections =+ | ((FaceIntersection px):_) <- ncl^.nclLpPlaneIntersections =+ -- Just a single planar intersection. So+ -- advance p, and mirror velocity about the+ -- plane.+ --+ -- TODO: handle body velocity! This for+ -- now just assumes the body is stationary.+ let plane = (px^.llpfiPlane) in+ -- If the velocity is going away from the+ -- plane due to a previous collision update+ -- in this step (i.e. we're on a check+ -- lump, not the first step), then skip.+ if' (not $ v `d3` (plane^.abcp3) <= 0) (p, v) $+ let p' = ipb (px^.llpfiIntersectionBall) (px^.llpfiBi) in+ let v' = plane3ReflectPointAmount (plane & dp3 .~ 0) v bounceReturn in+ (p', v')+ | ((EdgeIntersection px):_) <- ncl^.nclLpPlaneIntersections =+ -- An edge intersection.+ let plane = (px^.llpeiVirtualPlane) in+ if' (not $ v `d3` (plane^.abcp3) <= 0) (p, v) $+ let p' = ipb (px^.llpeiIntersectionBall) (px^.llpeiBi) in+ let v' = plane3ReflectPointAmount (plane & dp3 .~ 0) v bounceReturn in+ (p', v')+ | ((VertIntersection px):_) <- ncl^.nclLpPlaneIntersections =+ -- An edge intersection.+ let plane = (px^.llpviVirtualPlane) in+ if' (not $ v `d3` (plane^.abcp3) <= 0) (p, v) $+ let p' = ipb (px^.llpviIntersectionBall) (px^.llpviBi) in+ let v' = plane3ReflectPointAmount (plane & dp3 .~ 0) v bounceReturn in+ (p', v')++ -- | Now apply gravity after advancing to the next collision.+ afterNextCollisionGravity :: Maybe (Vec3 Double, Vec3 Double, Double)+ afterNextCollisionGravity = (<$> afterNextCollision) $ \(pn', vn', edt) ->+ (pn', vg vn' $ edt, edt)++ -- | The result of 'advance'. Try checking for collisions+ -- again if we found a collision, else we can finish.+ result :: (Vec3 Double, Vec3 Double)+ result+ | Just (pn', vn', edt) <- afterNextCollisionGravity =+ advance+ (numLocalCollisions + 1)+ (localDtExpended + edt)+ (localDistance + (pn' - pn)^.r3)+ (dtn - edt)+ pn'+ vn'+ | otherwise =+ (p1, v1)++-- * Utils.++-- | Given the current game state, and the given _path_ by index (not body+-- index), get the translation of the path you can add body vertices to to get+-- the coords in world coords.+getPathTranslation :: LevelIB -> SolOtherAnalysis -> GameState -> Int32 -> Integer -> Vec3 Double+getPathTranslation _level soa gs pi_ derivativeDegree =+ let translAtTime = M.lookup pi_ ((soa^.soaPathTranslationAtTime.fakeEOS) derivativeDegree) `morElse`+ --(error $ "Error: getPathTranslation: failed to find translation map for path " ++ show pi_ ++ ".")+ const zv3 in+ -- TODO: Implement updating pathsTimeElapsed when stepping, then invert the comments in the next 2 lines.+ let pathTimeElapsed = gs^.gsTimeElapsed in+ --let pathTimeElapsed = flip M.lookup (gs^.gsPathState.psPathsTimeElapsed) pi_ `morElse` 0.0 in+ let bodyTranslation = translAtTime pathTimeElapsed in+ bodyTranslation++-- | Given the current game state, and the given _body_ by index (not path+-- index), get the translation of the path you can add body vertices to to get+-- the coords in words coords.+getBodyTranslation :: LevelIB -> SolOtherAnalysis -> GameState -> Int32 -> Integer -> Vec3 Double+getBodyTranslation level soa gs bi derivativeDegree =+ let body = (level^.solBv) ! bi in+ let bodyPath = body^.bodyP0 in+ let bodyTranslation = getPathTranslation level soa gs bodyPath derivativeDegree in+ bodyTranslation -- * Local utils.
Immutaball/Ball/State/Play.hs view
@@ -28,6 +28,7 @@ import qualified SDL.Raw.Enum as Raw import Immutaball.Ball.Game+import Immutaball.Ball.Level import Immutaball.Ball.LevelSets import Immutaball.Ball.State.Game import Immutaball.Share.Context@@ -78,7 +79,7 @@ let (mview :: MView) = gameStateAnalysis^.gsaView isPaint <- returnA -< ((const False) ||| (const True)) . matching (_Paint) $ request cxtnp2 <- returnA ||| renderLevel -< if' (not isPaint) (Left cxtnp1) (Right $ ((mview, (gameState^.gsSwa), gameState), cxtnp1))- cxtnp3 <- returnA ||| renderBall -< if' (not isPaint) (Left cxtnp1) (Right $ (gameState, cxtnp2))+ cxtnp3 <- returnA ||| renderBall -< if' (not isPaint) (Left cxtnp1) (Right $ ((mview, gameState), cxtnp2)) -- GUI. Positioned after scene rendering. --(_guiResponse, cxtnp4) <- mkGUI playGui -< (GUIDrive request, cxtnp3)
Immutaball/Putt/CLI.hs view
@@ -143,7 +143,7 @@ ] immutaballVersion :: String-immutaballVersion = "0.1.0.4.1-hackage"+immutaballVersion = "0.1.0.5.1-hackage" -- | Run immutaball. immutaballWithCLIConfig :: StaticConfig -> CLIConfig -> ImmutaballIO
@@ -23,6 +23,7 @@ x'cfgGravity, x'cfgBounceReturn, x'cfgMaxFrameCollisions, x'cfgMaxFrameCollisionsDtThreshold, x'cfgMaxFrameCollisionsRDistanceThreshold, x'cfgMaxPhysicsStepTime,+ x'cfgRequestAlternativePhysics, defaultStaticConfig, Neverballrc, Config(..), fullscreen, display, width, height, stereo, camera,@@ -162,7 +163,10 @@ -- gravity at once, sending it flying. Already the ball is bouncy even -- with this, however, so TODO: improve the physics to fix the bouncy -- elevator/gravity problem.- _x'cfgMaxPhysicsStepTime :: Maybe Double+ _x'cfgMaxPhysicsStepTime :: Maybe Double,++ -- | Allow runtime configuration of the choice of physics engine.+ _x'cfgRequestAlternativePhysics :: Maybe String } makeLenses ''StaticConfig' @@ -226,7 +230,9 @@ --_x'cfgMaxPhysicsStepTime = Just 0.01 -- Smooths out the gravity bounce issue, but larger levels can't handle it. --_x'cfgMaxPhysicsStepTime = Just 0.2 -- More tolerant, but e.g. retour de force for me would only handle this (or even Nothing) if I -- -- disabled edge and vertices by commenting out the 2 lines in the concat list.- _x'cfgMaxPhysicsStepTime = Just 0.02+ _x'cfgMaxPhysicsStepTime = Just 0.02,++ _x'cfgRequestAlternativePhysics = Nothing } type Neverballrc = Config
@@ -9,7 +9,6 @@ module Immutaball.Share.Level ( module Immutaball.Share.Level.Base,- module Immutaball.Share.Level.Render, module Immutaball.Share.Level.Parser, module Immutaball.Share.Level.Utils ) where@@ -18,6 +17,5 @@ --import Immutaball.Prelude import Immutaball.Share.Level.Base-import Immutaball.Share.Level.Render import Immutaball.Share.Level.Parser import Immutaball.Share.Level.Utils
@@ -15,7 +15,7 @@ module Immutaball.Share.Level.Analysis ( SolWithAnalysis(..), swaSol, swaSa, swaMeta,- SolAnalysis(..), saRenderAnalysis, saPhysicsAnalysis,+ SolAnalysis(..), saRenderAnalysis, saPhysicsAnalysis, saOtherAnalysis, SolMeta(..), smPath, smLevelSet, sar, sap, SolRenderAnalysis(..), sraVertexData, sraVertexDataGPU, sraGeomData,@@ -32,14 +32,22 @@ sraGeomPassBis, sraGeomPassBisGPU, sraTexcoordsDoubleData, sraTexcoordsDoubleDataGPU, GeomPass(..), gpBi, gpMv, gpTextures, gpTexturesGPU, gpGis, gpGisGPU,+ LumpBSP(..), lumpBSP,+ LumpBSPPartition(..), lbsppPlane, lbsppLumps, lbsppLumpsMeanVertex,+ lbsppAllLumps, lbsppAllLumpsMeanVertex, SolPhysicsAnalysis(..), spaLumpOutwardsSides, spaLumpOutwardsSidesNumNegatedNormals, spaLumpOutwardsSidesNumNotNegatedNormals, spaLumpAverageVertex, spaLumpVertexAdjacents, {-spaLumpGetVertexAdjacents, -}spaLumpPlanes,+ spaBodyBSPs, spaBodyBSPNumPartitions, spaBSPNumPartitions,+ SolOtherAnalysis(..), soaPathAtTime, soaPathTransformationAtTime,+ soaPathTranslationAtTime, soaPathAtTimeMap, soaPathAtTimeRMap,+ soaPathCycle, mkSolAnalysis, mkSolRenderAnalysis, getSpaLumpGetVertexAdjacents,- mkSolPhysicsAnalysis+ mkSolPhysicsAnalysis,+ mkSolOtherAnalysis ) where import Prelude ()@@ -51,21 +59,23 @@ import Data.Foldable import Data.Function hiding (id, (.)) import Data.Int+import Data.List hiding (partition) import Data.Maybe import Control.Lens import Control.Monad.Trans.State.Lazy import Data.Array.IArray-import Data.List import qualified Data.Map.Lazy as M import qualified Data.Set as S +import Data.LabeledBinTree import Immutaball.Ball.LevelSets import Immutaball.Share.Config import Immutaball.Share.Context import Immutaball.Share.ImmutaballIO.GLIO --import Immutaball.Share.Level.Analysis.LowLevel import Immutaball.Share.Level.Base+import Immutaball.Share.Level.Utils import Immutaball.Share.Math import Immutaball.Share.Utils import Immutaball.Share.Video@@ -83,7 +93,10 @@ _saRenderAnalysis :: SolRenderAnalysis, -- | Extra analysis of the sol useful for physics.- _saPhysicsAnalysis :: SolPhysicsAnalysis+ _saPhysicsAnalysis :: SolPhysicsAnalysis,++ -- | Analysis that can be used by either rendering or physics.+ _saOtherAnalysis :: SolOtherAnalysis } deriving (Eq, Ord, Show) --makeLenses ''SolAnalysis@@ -231,6 +244,33 @@ deriving (Eq, Ord, Show) --makeLenses ''SolRenderAnalysis +newtype LumpBSP = LumpBSP {_lumpBSP :: Tree LumpBSPPartition}+ deriving (Eq, Ord, Show)+--makeLenses ''LumpBSP++-- | A planar partition of the remaining lumps.+data LumpBSPPartition = LumpBSPPartition {+ -- | The plane making the partition of the remaining lumps.+ _lbsppPlane :: Plane3 Double,+ -- | All lumps that intersect this plane. (Either not all vertices are on+ -- the same side of the plane, or there is a vertex that intersects the plane.)+ _lbsppLumps :: S.Set Int32,++ -- For convenience, we also provide more data.++ -- | The average vertex of lumps intersecting this plane.+ _lbsppLumpsMeanVertex :: Vec3 Double,++ -- | Optionally, we can also reference all remaining lumps: i.e. all lumps+ -- on this plane, and all lumps on any child node.+ _lbsppAllLumps :: S.Set Int32,+ -- | The average vertex of all lumps remaining, both on this node and all+ -- child nodes.+ _lbsppAllLumpsMeanVertex :: Vec3 Double+}+ deriving (Eq, Ord, Show)+--makeLenses ''LumpBSPPartition+ -- | Extra data of the sol useful for physics. data SolPhysicsAnalysis = SolPhysicsAnalysis { -- | OLD: actually SOL file lump sides I found were not the actual planes@@ -264,15 +304,63 @@ -- | For each lump, we build from the edges and vertices a set of planes -- with normals pointing away from the convex lump.- _spaLumpPlanes :: M.Map Int32 [Plane3 Double]+ _spaLumpPlanes :: M.Map Int32 [Plane3 Double],++ -- | Map from body indices to BSPs of those bodies. Bodies in a Sol are+ -- sets of lumps that all follow the same translation and rotation path.+ _spaBodyBSPs :: M.Map Int32 LumpBSP,++ -- | For each body, how many partitions does it have?+ _spaBodyBSPNumPartitions :: M.Map Int32 Integer,+ -- | What is the total number of partitions for all bodies?+ -- This is intended to be used with ‘par’ to parallelize an early+ -- evaluation of all BSPs.+ _spaBSPNumPartitions :: Integer } deriving (Eq, Ord, Show)+--makeLenses ''SolPhysicsAnalysis++-- | Other level analysis, e.g. body calculations for renderer or physics.+data SolOtherAnalysis = SolOtherAnalysis {+ -- | Given an origin path (by index), obtain a function from time elapsed+ -- to the current path any attached body would be on, along with a value+ -- from 0 to 1 representing how far along the path that body would be at+ -- that time.+ -- Currently only translations are supported.+ _soaPathAtTime :: FakeEOS (M.Map Int32 (Double -> (Int32, Double))),+ -- | Likewise, but obtain a transformation matrix for the base path,+ -- without hierarchical paths.+ _soaPathTransformationAtTime :: FakeEOS (M.Map Int32 (Double -> Mat4 Double)),+ -- | Offset to apply to the vertices. Also takes number of derivatives to+ -- obtain e.g. velocity and acceleration if desired; for position pass 0.+ -- Note: beware if derivativeDegree is 1, and the path has travel time 0,+ -- there could be an infinite / invalid result; consider 'v3Or', especially+ -- in the physics implementation.+ _soaPathTranslationAtTime :: FakeEOS (Integer -> M.Map Int32 (Double -> Vec3 Double)),++ -- | Intermediate structure; soaPathAtTime abstracts this.+ -- For each path, find the time elapsed (and node index) for each unique+ -- node until the end or a cycle is detected.+ _soaPathAtTimeMap :: M.Map Int32 (M.Map (Double, Integer) Int32),+ _soaPathAtTimeRMap :: M.Map Int32 (M.Map Int32 (Double, Integer)),+ -- | Is there a cycle in the path, ignoring last node self-reference?+ -- If the last distinct node (‘from’) in the path returns to an earlier+ -- node ‘to’, this is (to, cycle period, from), i.e. (cycle start, cycle+ -- period, cycle last node).+ _soaPathCycle :: M.Map Int32 (Maybe (Int32, Double, Int32))+}+ deriving (Eq, Ord, Show) -- We'll use FakeEOS because we use functions here.+--makeLenses ''SolOtherAnalysis+ makeLenses ''SolWithAnalysis makeLenses ''SolAnalysis makeLenses ''SolMeta makeLenses ''SolRenderAnalysis+makeLenses ''LumpBSP+makeLenses ''LumpBSPPartition makeLenses ''GeomPass makeLenses ''SolPhysicsAnalysis+makeLenses ''SolOtherAnalysis sar :: Lens' SolAnalysis SolRenderAnalysis sar = saRenderAnalysis@@ -283,7 +371,8 @@ mkSolAnalysis :: IBContext' a -> Sol -> SolAnalysis mkSolAnalysis cxt sol = fix $ \_sa -> SolAnalysis { _saRenderAnalysis = mkSolRenderAnalysis cxt sol,- _saPhysicsAnalysis = mkSolPhysicsAnalysis cxt sol+ _saPhysicsAnalysis = mkSolPhysicsAnalysis cxt sol,+ _saOtherAnalysis = mkSolOtherAnalysis cxt sol } mkSolRenderAnalysis :: IBContext' a -> Sol -> SolRenderAnalysis@@ -536,7 +625,7 @@ getSpaLumpGetVertexAdjacents spa = \li vi -> (M.lookup li (spa^.spaLumpVertexAdjacents) >>= M.lookup vi) `morElse` S.empty mkSolPhysicsAnalysis :: IBContext' a -> Sol -> SolPhysicsAnalysis-mkSolPhysicsAnalysis _cxt sol = fix $ \spa -> SolPhysicsAnalysis {+mkSolPhysicsAnalysis _cxt sol = fix $ \spa -> SolPhysicsAnalysis { -- TODO _spaLumpOutwardsSides = (^._1) <$> lumpSidesData spa, _spaLumpOutwardsSidesNumNegatedNormals = (^._2) <$> lumpSidesData spa, _spaLumpOutwardsSidesNumNotNegatedNormals = (^._3) <$> lumpSidesData spa,@@ -547,7 +636,12 @@ {- _spaLumpGetVertexAdjacents = lumpGetVertexAdjacents spa, -}- _spaLumpPlanes = lumpPlanes spa+ _spaLumpPlanes = lumpPlanes spa,++ _spaBodyBSPs = bodyBSPs spa,++ _spaBodyBSPNumPartitions = (numElemsLBTI . (^.lumpBSP)) <$> (spa^.spaBodyBSPs),+ _spaBSPNumPartitions = sum $ M.elems (spa^.spaBodyBSPNumPartitions) } where indirection :: Int32 -> Int32@@ -631,7 +725,7 @@ let r = plane return $ r in -- De-duplicate the planes.- let planesDedup = nubBy eqPlane3PointsOnly $ planesStart in+ let planesDedup = nubBy nearPlane3PointsOnly $ planesStart in -- Now orient the planes, so that the normal is pointing outwards -- from the convex lump. That is, negate the normal orientation of@@ -646,3 +740,306 @@ -- Return the planes. (li, planesOriented)++ -- | Make a BSP of each body at runtime.+ bodyBSPs :: SolPhysicsAnalysis -> M.Map Int32 LumpBSP+ bodyBSPs spa = M.fromList . flip map [0..sol^.solBc - 1] $ \bi ->+ let body = (sol^.solBv) ! bi in+ let bodyLumpIndicesRaw = [body^.bodyL0 .. body^.bodyL0 + body^.bodyLc - 1] in+ -- Discard detail lumps.+ let bodyLumpIndices = [li | li <- bodyLumpIndicesRaw, let lump = (sol^.solLv) ! li, ((lump^.lumpFl) .&. lumpFlagDetail) == 0] in+ let (firstPartition :: LumpBSPPartition) = fix $ \partition -> LumpBSPPartition {+ _lbsppPlane =+ let allMean = (partition^.lbsppAllLumpsMeanVertex) in+ let allLumps = (partition^.lbsppAllLumps) in+ let initialNormal = v3normalize . correctNormal $ (Vec3 1 0 0) in+ -- Make sure there is always at least 1 lump in a+ -- partition: instead of using allMean for the point for+ -- our plane, find the closest lump to the mean and use its+ -- mean.+ let allMean' = closestLumpMean allLumps allMean in+ normalizePlane3 allMean' (refineNormal allLumps allMean' initialNormal),+ _lbsppLumps = S.filter (lumpIntersectsPlane (partition^.lbsppPlane)) (partition^.lbsppAllLumps),++ _lbsppLumpsMeanVertex = lumpsAverageVertex (partition^.lbsppLumps),++ _lbsppAllLumps = S.fromList bodyLumpIndices,+ _lbsppAllLumpsMeanVertex = lumpsAverageVertex (partition^.lbsppAllLumps)+ } in+ (,) bi . LumpBSP . normalizeLabeledBinTree .+ (\f -> (f :: LumpBSPPartition -> Tree LumpBSPPartition) firstPartition) . fix $ \makeTree ->+ \parentPartition ->+ if' (S.null (parentPartition^.lbsppAllLumps)) (leafLBT parentPartition) $+ forkLBT+ (makeTree . fix $ \partition -> LumpBSPPartition {+ _lbsppPlane =+ let allMean = (partition^.lbsppAllLumpsMeanVertex) in+ let allLumps = (partition^.lbsppAllLumps) in+ let initialNormal = v3normalize . correctNormal $ (parentPartition^.lbsppPlane.abcp3) `vx3` ((partition^.lbsppAllLumpsMeanVertex) - (parentPartition^.lbsppAllLumpsMeanVertex)) in+ -- Make sure there is always at least 1 lump in a+ -- partition: instead of using allMean for the point for+ -- our plane, find the closest lump to the mean and use its+ -- mean.+ let allMean' = closestLumpMean allLumps allMean in+ normalizePlane3 allMean' (refineNormal allLumps allMean' initialNormal),+ _lbsppLumps = S.filter (lumpIntersectsPlane (partition^.lbsppPlane)) (partition^.lbsppAllLumps),++ _lbsppLumpsMeanVertex = lumpsAverageVertex (partition^.lbsppLumps),++ _lbsppAllLumps = S.filter (\li -> lumpPlaneSide (parentPartition^.lbsppPlane) li == (-1)) (parentPartition^.lbsppAllLumps),+ _lbsppAllLumpsMeanVertex = lumpsAverageVertex (partition^.lbsppAllLumps)+ })+ (parentPartition & lbsppLumps %~ assertNonNull)+ (makeTree . fix $ \partition -> LumpBSPPartition {+ _lbsppPlane =+ let allMean = (partition^.lbsppAllLumpsMeanVertex) in+ let allLumps = (partition^.lbsppAllLumps) in+ let initialNormal = v3normalize . correctNormal $ (parentPartition^.lbsppPlane.abcp3) `vx3` ((partition^.lbsppAllLumpsMeanVertex) - (parentPartition^.lbsppAllLumpsMeanVertex)) in+ -- Make sure there is always at least 1 lump in a+ -- partition: instead of using allMean for the point for+ -- our plane, find the closest lump to the mean and use its+ -- mean.+ let allMean' = closestLumpMean allLumps allMean in+ normalizePlane3 allMean' (refineNormal allLumps allMean' initialNormal),+ _lbsppLumps = S.filter (lumpIntersectsPlane (partition^.lbsppPlane)) (partition^.lbsppAllLumps),++ _lbsppLumpsMeanVertex = lumpsAverageVertex (partition^.lbsppLumps),++ _lbsppAllLumps = S.filter (\li -> lumpPlaneSide (parentPartition^.lbsppPlane) li == 1) (parentPartition^.lbsppAllLumps),+ _lbsppAllLumpsMeanVertex = lumpsAverageVertex (partition^.lbsppAllLumps)+ })++ where+ lumpsAverageVertex :: S.Set Int32 -> Vec3 Double+ lumpsAverageVertex lumps =+ steppingMean .+ --setMapFilter (\li -> flip M.lookup (spa^.spaLumpAverageVertex) li) $+ S.map (\x -> x `morElse` error "Internal error: mkSolPhysicsAnalysis lumpsAverageVertex failed to find average vertex for a lump") .+ S.map (\li -> flip M.lookup (spa^.spaLumpAverageVertex) li) $+ lumps++ lumpIntersectsPlane :: Plane3 Double -> Int32 -> Bool+ {-+ lumpIntersectsPlane plane li =+ let lump = (sol^.solLv) ! li in+ let vis = indirection <$> [lump^.lumpV0..lump^.lumpV0 + lump^.lumpVc - 1] in+ let vs = (^.vertP) . ((sol^.solVv) !) <$> vis in+ -- Which side is each vertex on?+ let sides = map thresholdSignnum . map (plane3PointDistance plane) $ vs in+ let anyVertOnPlane = any (== 0) sides in+ let lumpCrossesPlane = length (nub sides) > 1 in+ anyVertOnPlane || lumpCrossesPlane+ -}+ lumpIntersectsPlane plane li = lumpPlaneSide plane li == 0++ -- | Does the lump intersect the plane? Classify as 0.+ -- Otherwise, classify by whether all lump vertices are behind (-1) or+ -- in front of (1) the plane.+ lumpPlaneSide :: Plane3 Double -> Int32 -> Integer+ lumpPlaneSide plane li =+ let lump = (sol^.solLv) ! li in+ let vis = indirection <$> [lump^.lumpV0..lump^.lumpV0 + lump^.lumpVc - 1] in+ let vs = (^.vertP) . ((sol^.solVv) !) <$> vis in+ -- Which side of the plane is each vertex on?+ let sides = map thresholdSignnumI . map (plane3PointDistance plane) $ vs in+ let anyVertOnPlane = any (== 0) sides in+ let lumpCrossesPlane = length (nub sides) > 1 in+ if' (anyVertOnPlane || lumpCrossesPlane || null sides) ( 0) .+ if' (all (<= 0) sides) (-1) $+ id ( 1)++ -- | If the normal is small, default to Vec3 1 0 0; otherwise, normalize it.+ correctNormal :: Vec3 Double -> Vec3 Double+ correctNormal v+ | (v^.r3) - smallishNum <= threshold = v3normalize $ Vec3 1 0 0+ | otherwise = v3normalize $ v+ where threshold = 0.001++ -- | Given a set of lumps, the mean mean vertex of those lumps,+ -- and an initial normal, try to produce a better normal that+ -- minimizes the difference in size between lumps in front of+ -- and behind the plane ‘normalizePlane3 mean normal’. i.e. we+ -- made a guess as to a good partition of the lumps, and try to+ -- refine the normal of the plane to equalize the number of+ -- lumps of each side.+ refineNormal :: S.Set Int32 -> Vec3 Double -> Vec3 Double -> Vec3 Double+ refineNormal lis mean lastNormal0 =+ (\f -> (f :: Integer -> Vec3 Double -> Vec3 Double) maxIterations lastNormal0) . fix $ \iterate_ iterationsRemaining lastNormal ->+ if' (iterationsRemaining <= 0) (lastNormal) .+ if' (inequality' lastNormal <= 1) (lastNormal) $+ let tryNormal = lumpsAverageVertex . flip S.filter lis $ \li -> lumpPlaneSide (normalizePlane3 mean lastNormal) li == 1 in+ if' (inequality' tryNormal > inequality' lastNormal) (lastNormal) $+ iterate_ (iterationsRemaining - 1) tryNormal+ where+ maxIterations :: Integer+ maxIterations = 8++ -- | Given a normal, what is the difference in number+ -- of lumps on each side?+ _inequality :: Vec3 Double -> Integer+ _inequality tryNormal =+ let sidesNotOn = S.filter (/= 0) . S.map (lumpPlaneSide (normalizePlane3 mean tryNormal)) $ lis in+ let numBehind = setSize . S.filter (<= 0) $ sidesNotOn in+ let numAhead = setSize sidesNotOn - numBehind in+ abs $ numAhead - numBehind++ setSize :: S.Set a -> Integer+ setSize = fromIntegral . S.size++ -- | Adjust inequality by adding a cost of having more+ -- lumps (after the first) _on_ the plane, since these+ -- always have to checked before descending in the tree for e.g.+ -- collisions.+ inequality' :: Vec3 Double -> Integer+ inequality' tryNormal =+ let sidesNotOn = S.filter (/= 0) . S.map (lumpPlaneSide (normalizePlane3 mean tryNormal)) $ lis in+ let numBehind = setSize . S.filter (<= 0) $ sidesNotOn in+ let numAhead = setSize sidesNotOn - numBehind in+ let numOn = setSize lis - setSize sidesNotOn in+ (abs $ numAhead - numBehind) + 2 * numOn++ -- | Given a set of lumps and a point, find the lump whose mean+ -- is closest to that point, and return its mean.+ closestLumpMean :: S.Set Int32 -> Vec3 Double -> Vec3 Double+ closestLumpMean allLumps allMean = (safeHead . sortOn (\mean -> (mean - allMean)^.r3) . map liToMean . S.toList $ allLumps) `morElse` allMean+ where liToMean li = flip M.lookup (spa^.spaLumpAverageVertex) li `morElse` error ("Internal error: mkSolPhysicsAnalysis closestLumpMean could not find mean vertex of lump ‘" ++ (show li) ++ "’.")++ assertNonNull :: S.Set a -> S.Set a+ assertNonNull s = if' (S.null s) (error "Internal error: mkSolPhysicsAnalysis bodyBSPs assertNonNull: each non-leaf partition should have at least 1 lump, but this partition didn't!") (s)++-- | Given a sol, construct a 'SolOtherAnalysis' from it.+-- TODO+mkSolOtherAnalysis :: IBContext' a -> Sol -> SolOtherAnalysis+mkSolOtherAnalysis _cxt sol = fix $ \soa -> SolOtherAnalysis {+ _soaPathAtTime = FakeEOS $ theSoaPathAtTime soa,+ _soaPathTransformationAtTime = FakeEOS $ theSoaPathTransformationAtTime soa,+ _soaPathTranslationAtTime = FakeEOS $ theSoaPathTranslationAtTime soa,+ _soaPathAtTimeMap = theSoaPathAtTimeMap soa,+ _soaPathAtTimeRMap = theSoaPathAtTimeRMap soa,+ _soaPathCycle = theSoaPathCycle soa+}+ where+ theSoaPathAtTime :: SolOtherAnalysis -> M.Map Int32 (Double -> (Int32, Double))+ theSoaPathAtTime soa = (`M.mapWithKey` (soa^.soaPathAtTimeMap)) $ \pi_ _atTimeMap -> \t ->+ -- First check if ‘t’ has entered a cycle.+ let (mcycleDuration :: Maybe Double) = do+ pathCyclePi <- flip M.lookup (soa^.soaPathCycle) pi_+ cycleDuration <- (^._2) <$> pathCyclePi+ return cycleDuration in+ let (mcycleStartAt :: Maybe Double) = do+ pathCyclePi <- flip M.lookup (soa^.soaPathCycle) $ pi_+ cycleStart <- (^._1) <$> pathCyclePi+ atTimeR <- flip M.lookup (soa^.soaPathAtTimeRMap) $ pi_+ (cycleStartAt, _cycleStartIndex) <- flip M.lookup atTimeR $ cycleStart+ return cycleStartAt in+ let (isCycle :: Bool) = ((t >=) <$> mcycleStartAt) == Just True in+ -- If there's a cycle, just wrap t around the cycle period to loop back.+ let (t' :: Double) = (`morElse` t) $ do+ cycleDuration <- mcycleDuration+ cycleStartAt <- mcycleStartAt+ guard $ isCycle+ let (timeOnCycle :: Double) = t - cycleStartAt+ let (wrappedTimeOnCycle :: Double) = timeOnCycle `modfl` cycleDuration+ return $ cycleStartAt + wrappedTimeOnCycle in+ -- Now find the node it's on, and turn it into a path and time elapsed.+ (`morElse` (pi_, 0.0)) $ do+ pathAtTimeMap <- flip M.lookup (soa^.soaPathAtTimeMap) pi_+ (k, v) <- flip M.lookupLE pathAtTimeMap (t', -1)+ let (nodeStarts :: Double) = k^._1+ let (node :: Int32) = v+ let (timeOnNode :: Double) = t' - nodeStarts++ guard . inRange (bounds $ sol^.solPv) $ node+ let (path :: Path) = (sol^.solPv) ! node+ let (pathTime :: Double) = path^.pathT+ -- progressOnNode: from 0 to 1, for 0 at start, 1 at end.+ let (progressOnNode :: Double) = ilerp 0.0 pathTime timeOnNode++ return $ (node, progressOnNode)++ theSoaPathTransformationAtTime :: SolOtherAnalysis -> M.Map Int32 (Double -> Mat4 Double)+ theSoaPathTransformationAtTime soa = (<$> (soa^.soaPathTranslationAtTime.fakeEOS) 0) $ \f -> \t ->+ let (netPosCorrected :: Vec3 Double) = f t in++ let (translate :: Mat4 Double) = translate3 netPosCorrected in++ translate++ theSoaPathTranslationAtTime :: SolOtherAnalysis -> Integer -> M.Map Int32 (Double -> Vec3 Double)+ theSoaPathTranslationAtTime soa = \derivativeDegree -> (`M.mapWithKey` (soa^.soaPathAtTime.fakeEOS)) $ \pi_ atTime -> \t ->+ (`morElse` zv3) $ do+ -- Get origin path.+ guard . inRange (bounds $ sol^.solPv) $ pi_ -- Redundant.+ let (originPath :: Path) = (sol^.solPv) ! pi_++ -- Get the path that would be current at time t, relative to+ -- the origin path.+ let (onNode, (progressOnNode :: Double)) = atTime t+ guard . inRange (bounds $ sol^.solPv) $ onNode -- Redundant.+ let (onPath :: Path) = (sol^.solPv) ! onNode++ let nextNode = onPath^.pathPi+ guard . inRange (bounds $ sol^.solPv) $ nextNode+ let (nextPath :: Path) = (sol^.solPv) ! nextNode++ let (originPos :: Vec3 Double) = originPath^.pathP+ let (pathPos :: Vec3 Double) = onPath^.pathP+ let (nextPos :: Vec3 Double) = nextPath^.pathP+ let (lerpPos :: Vec3 Double)+ | derivativeDegree == 0 = lerpV3 pathPos nextPos progressOnNode+ -- If derivativeDegre is 1, and the path has travel time0,+ -- beware this could give us an infinite or invalid+ -- velocity vector.+ | derivativeDegree == 1 = (nextPos - pathPos)/(rv3$onPath^.pathT)+ | otherwise = zv3+ let (solErpPos :: Vec3 Double)+ | derivativeDegree == 0 = lerpV3 pathPos nextPos . solErp $ progressOnNode+ | otherwise =+ solErpdn derivativeDegree progressOnNode `sv3` ((nextPos - pathPos)/(rv3$onPath^.pathT))++ let (selectLerpPos :: Vec3 Double) = if' (onPath^.pathS /= 0) solErpPos lerpPos++ let (netPos :: Vec3 Double) = selectLerpPos - originPos+ let (netPosCorrected :: Vec3 Double) = netPos + originPos -- SOL seems to require adding originPos to get the correct positioning.++ return $ netPosCorrected++ -- | Traverse each path until the end is reached or there is a cycle.+ -- Record how much total time elapsed would be required to start each+ -- node in the path.+ theSoaPathAtTimeMap :: SolOtherAnalysis -> M.Map Int32 (M.Map (Double, Integer) Int32)+ theSoaPathAtTimeMap _soa = M.fromList . flip map [0..(sol^.solPc)-1] $ \pi_ -> (,) pi_ .+ M.fromList . flip fix (0.0, S.empty, pi_, 0) $ \me (elapsed, visited, pni, nodeIndex) ->+ if' (pni `S.member` visited) [] .+ if' (not . inRange (bounds $ sol^.solPv) $ pni) [] $+ let pn = (sol^.solPv) ! pni in+ let node = ((elapsed, nodeIndex), pni) in+ node : me (elapsed + pn^.pathT, S.insert pni visited, pn^.pathPi, nodeIndex + 1)+ theSoaPathAtTimeRMap :: SolOtherAnalysis -> M.Map Int32 (M.Map Int32 (Double, Integer))+ theSoaPathAtTimeRMap soa = (M.fromList . fmap swap . M.toList) <$> (soa^.soaPathAtTimeMap)++ -- | For each path, check for a cycle: (to, cycle time elapsed, from).+ theSoaPathCycle :: SolOtherAnalysis -> M.Map Int32 (Maybe (Int32, Double, Int32))+ theSoaPathCycle soa = (`M.mapWithKey` (soa^.soaPathAtTimeMap)) $ \pi_ atTime -> do+ timeAtPathMap <- flip M.lookup (soa^.soaPathAtTimeRMap) $ pi_+ (lastNodeKey :: (Double, Integer)) <- S.lookupMax . M.keysSet $ atTime+ (lastNode :: Int32) <- flip M.lookup atTime lastNodeKey+ guard . inRange (bounds $ sol^.solPv) $ lastNode -- Redundant; lastNode should be valid, but not necessarily lastNodeNext.+ let lastPath = (sol^.solPv) ! lastNode+ guard $ (lastPath^.pathPi) /= lastNode++ let lastNodeNext = lastPath^.pathPi+ guard . inRange (bounds $ sol^.solPv) $ lastNodeNext++ (lastNodeNextKey :: (Double, Integer)) <- flip M.lookup timeAtPathMap lastNodeNext++ -- From start of earlier to start of last (to - from).+ let (earlierToLastDuration :: Double) = (lastNodeKey^._1) - (lastNodeNextKey^._1)+ -- Now just add the duration of the last node itself.+ let (lastNodeDuration :: Double) = lastPath^.pathT+ let (cycleDuration :: Double) = earlierToLastDuration + lastNodeDuration++ -- (cycle start, cycle period, cycle last node)+ let (to_, cyclePeriod, from_) = (lastNodeNext, cycleDuration, lastNode)+ let (cycleStart, cyclePeriod', cycleLastNode) = (to_, cyclePeriod, from_)+ return (cycleStart, cyclePeriod', cycleLastNode)
@@ -1,265 +0,0 @@-{-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .--- Enable warnings:-{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}---- Level/Render.hs.--{-# LANGUAGE Haskell2010 #-}-{-# LANGUAGE Arrows, ScopedTypeVariables #-}---- | TODO: split shared render into shared and ball render.------ Currently this uses ball game state.-module Immutaball.Share.Level.Render- (- renderLevel,- renderSetupNewLevel,- renderScene,- renderGeomPass- ) where--import Prelude ()-import Immutaball.Prelude--import Control.Arrow-import Control.Monad-import Data.Int-import Data.Ix-import Data.Maybe--import Control.Lens-import Data.Array.IArray-import qualified Data.Map.Lazy as M-import Graphics.GL.Compatibility45---import Graphics.GL.Core45-import Graphics.GL.Types--import Immutaball.Ball.Game-import Immutaball.Share.Config-import Immutaball.Share.Context-import Immutaball.Share.ImmutaballIO.GLIO-import Immutaball.Share.Level.Analysis-import Immutaball.Share.Level.Base-import Immutaball.Share.Math-import Immutaball.Share.SDLManager-import Immutaball.Share.State-import Immutaball.Share.State.Context-import Immutaball.Share.Utils-import Immutaball.Share.Video-import Immutaball.Share.Wire--renderLevel :: Wire ImmutaballM ((MView, SolWithAnalysis, GameState), IBStateContext) IBStateContext-renderLevel = proc ((camera_, swa, gs), cxtn) -> do- -- Set up.- let levelPath = swa^.swaMeta.smPath- (mlastLevelPath, cxtnp1) <- setCurrentlyLoadedSOL -< (levelPath, cxtn)- let newLevel = not $ Just levelPath == mlastLevelPath- cxtnp2 <- returnA ||| renderSetupNewLevel -< if' (not newLevel) (Left cxtnp1) (Right (swa, cxtnp1))-- -- Render the scene.- cxtnp3 <- renderScene -< ((camera_, swa, gs), cxtnp2)-- -- Return the state context.-- let cxt = cxtnp3- returnA -< cxt--renderSetupNewLevel :: Wire ImmutaballM (SolWithAnalysis, IBStateContext) IBStateContext-renderSetupNewLevel = proc (swa, cxtn) -> do- let sra = swa^.swaSa.saRenderAnalysis-- -- Upload SSBOs.- cxtnp1 <- setSSBO -< ((shaderSSBOVertexDataLocation, sra^.sraVertexDataGPU), cxtn)- cxtnp2 <- setSSBO -< ((shaderSSBOGeomDataLocation, sra^.sraGeomDataGPU), cxtnp1)- cxtnp3 <- setSSBO -< ((shaderSSBOLumpDataLocation, sra^.sraLumpDataGPU), cxtnp2)- cxtnp4 <- setSSBO -< ((shaderSSBOPathDoublesDataLocation, sra^.sraPathDoublesDataGPU), cxtnp3)- cxtnp5 <- setSSBO -< ((shaderSSBOPathInt32sDataLocation, sra^.sraPathInt32sDataGPU), cxtnp4)- cxtnp6 <- setSSBO -< ((shaderSSBOBodyDataLocation, sra^.sraBodyDataGPU), cxtnp5)- cxtnp7 <- setSSBO -< ((shaderSSBOGcDataLocation, sra^.sraGcArrayGPU), cxtnp6)- cxtnp8 <- setSSBO -< ((shaderSSBOAllGeomPassMvDataLocation, sra^.sraAllGeomPassMvGPU), cxtnp7)- cxtnp9 <- setSSBO -< ((shaderSSBOAllGeomPassTexturesDataLocation, sra^.sraAllGeomPassTexturesGPU), cxtnp8)- cxtnp10 <- setSSBO -< ((shaderSSBOAllGeomPassGisDataLocation, sra^.sraAllGeomPassGisGPU), cxtnp9)- cxtnp11 <- setSSBO -< ((shaderSSBOGeomPassMvRangesDataLocation, sra^.sraGeomPassMvRangesGPU), cxtnp10)- cxtnp12 <- setSSBO -< ((shaderSSBOGeomPassTexturesRangesDataLocation, sra^.sraGeomPassTexturesRangesGPU), cxtnp11)- cxtnp13 <- setSSBO -< ((shaderSSBOGeomPassGisRangesDataLocation, sra^.sraGeomPassGisRangesGPU), cxtnp12)- cxtnp14 <- setSSBO -< ((shaderSSBOGeomPassBisDataLocation, sra^.sraGeomPassBisGPU), cxtnp13)- cxtnp15 <- setSSBO -< ((shaderSSBOTexcoordsDoubleDataLocation, sra^.sraTexcoordsDoubleDataGPU), cxtnp14)-- -- Upload elems vao and buf.- cxtnp16 <- setElemVaoVboEbo -< (sra^.sraGcArrayGPU, True, cxtnp15)-- -- Pre-initialize the transformation matrix with the identity.- cxtnp17 <- setTransformation -< (identity4, cxtnp16)-- -- Approximate disabling OpenGL clipping by depth (by our transformed y coordinates, which transform into OpenGL z depth coordinates).- -- Also other miscellaneous OpenGL setup.- let sdlGL1'_ = sdlGL1 (cxtnp17^.ibContext.ibSDLManagerHandle)- let sdlGL1' = liftIBIO . sdlGL1'_- () <- monadic -< sdlGL1' $ do- -- TODO FIXME: this doesn't seem to actually stop clipping for depth- -- values outside a certain range.- --- -- To investigate, disable rescaleDepth, try retour de force level 2,- -- enable free camera, and rotate the camera until you can see the- -- level. Move toward that location, and then you can see that the- -- distance from the camera needs to be only within a certain range.- --- -- So instead we'll just use rescaleDepth.- GLDepthRange (cxtnp17^.ibContext.ibStaticConfig.x'glNearVal) (cxtnp17^.ibContext.ibStaticConfig.x'glFarVal) ()- -- Other miscellaneous OpenGL setup.- () <- monadic -< sdlGL1' $ do- --GLPlaceholder GL_VAL ()- pure ()-- -- Return the state context.-- let cxt = cxtnp17-- returnA -< cxt---- | After setup, render the scene.-renderScene :: Wire ImmutaballM ((MView, SolWithAnalysis, GameState), IBStateContext) IBStateContext-renderScene = proc ((camera_, swa, gs), cxtn) -> do- -- Set up the transformation matrix.- --let mat = transformationMatrix camera_- mat <- arr $ uncurry3 transformationMatrix -< (cxtn, gs, camera_)- cxtnp1 <- setTransformation -< (mat, cxtn)-- -- Render the scene.- let- sol :: Sol- sol = swa^.swaSol-- sa :: SolAnalysis- sa = swa^.swaSa-- sra :: SolRenderAnalysis- sra = sa^.saRenderAnalysis- let _unused = (sol)-- let- ourFlatten :: (Int32, (SolWithAnalysis, GameState, Bool, GeomPass)) -> (Int32, SolWithAnalysis, GameState, Bool, GeomPass)- ourFlatten (a, (b, c, d, e)) = (a, b, c, d, e)- let geomPasses = map ourFlatten . zip [0..] $ map (\gp -> (swa, gs, False, gp)) (sra^.sraOpaqueGeoms) ++ map (\gp -> (swa, gs, True, gp)) (sra^.sraTransparentGeoms)- cxtnp2 <- foldlA renderGeomPass -< (cxtnp1, geomPasses)-- -- Return the state context.- let cxt = cxtnp2-- returnA -< cxt-- where- uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d- uncurry3 f (a, b, c) = f a b c-- transformationMatrix :: IBStateContext -> GameState -> MView -> Mat4 Double- transformationMatrix cxt gs view_ = worldToGL <> rescaleDepth depthScale 0 <> viewMat' viewCollapse view_ <> tilt- where- x'cfg = cxt^.ibContext.ibStaticConfig- depthScale = x'cfg^.x'cfgDepthScale- viewCollapse = x'cfg^.x'cfgViewCollapse-- -- Translate to the ball (negated actually), rotate by gsCameraAngle, tilt3z, and untranslate.- tilt =- translate3 (gs^.gsBallPos) <> -- Finally, undo the first translation.- tilt3z (gsa^.gsaUpVec) <> -- Tilt the world.- rotatexy (-gs^.gsCameraAngle) <> -- Rotate camera.- translate3 (-gs^.gsBallPos) -- First, go to ball (negated actually).- gsa = mkGameStateAnalysis cxt gs---- | Render a partition of the level geometry, so that we can handle processing up to 16 textures at a time.-renderGeomPass :: Wire ImmutaballM (IBStateContext, (Int32, SolWithAnalysis, GameState, Bool, GeomPass)) IBStateContext-renderGeomPass = proc (cxtn, (geomPassIdx, swa, _gs, isAlpha, gp)) -> do- -- Setup.- let sdlGL1'_ = sdlGL1 (cxtn^.ibContext.ibSDLManagerHandle)- let sdlGL1' = liftIBIO . sdlGL1'_-- -- Render the geom pass.-- -- Render all 16 mtrls in the geom pass.- (mtrlsMeta :: [((WidthHeightI, GLuint), MtrlMeta)], cxtnp1) <-- foldrA cachingRenderMtrlAccum -< (([], cxtn), map (\mi -> (swa^.swaSol, mi)) (elems (gp^.gpMv)))- let (mtrlsGlTextures :: [GLuint]) = map (fst >>> snd) mtrlsMeta- let (assignTextures :: GLIOF ()) = do- forM_ (zip [0..] mtrlsGlTextures) $ \(idx, glTexture) -> do- case flip M.lookup numToGL_TEXTUREi idx of- Nothing -> return ()- Just gl_TEXTUREi -> do- -- Apparently actually we set GL_TEXTUREi for the texture()- -- GLSL call, not the texture name. So set the i used in- -- GL_TEXTUREi as the value, not the texture identifier.- --GLUniform1i (fromIntegral idx) (fromIntegral texture) ()- GLUniform1i (fromIntegral idx) (fromIntegral idx) ()-- --GLActiveTexture GL_TEXTUREi ()- GLActiveTexture gl_TEXTUREi ()- GLBindTexture GL_TEXTURE_2D glTexture ()-- -- Render all geometry in this geom pass.- (melemVaoVboEbo, cxtnp2) <- getElemVaoVboEbo -< cxtnp1- let elemVaoVboEbo = fromMaybe (error "Internal error: renderGeomPass expected elem vao and buf to be present, but it's missing!") melemVaoVboEbo- let (elemVao, _elemVbo, _elemEbo) = elemVaoVboEbo- let (renderGeomPassScene :: GLIOF ()) = do- -- Tell the shaders to enable the scene data.- GLUniform1i shaderEnableSceneDataLocation trueAsIntegral ()- -- Tell the shaders we are not drawing the ball right now.- GLUniform1i shaderEnableBallDataLocation falseAsIntegral ()-- -- Tell the shaders what geom pass to use.- GLUniform1i (shaderSceneGeomPassIdxLocation) (fromIntegral geomPassIdx) ()-- -- Tell the shaders to render this pass's geometries. It will handle the rest; it already has the geom pass data we uploaded upon setup.- GLBindVertexArray elemVao ()-- -- Use the vao to tell the shader to draw the geometry.- let numGpGis = rangeSize . bounds $ gp^.gpGis- GLDrawArrays GL_TRIANGLES 0 (fromIntegral (3 * numGpGis)) ()-- -- Unbind the VAO.- GLBindVertexArray 0 ()-- let (alphaSetup :: GLIOF ()) = do- if isAlpha- then do- GLEnable GL_BLEND ()- GLBlendEquationSeparate GL_FUNC_ADD GL_FUNC_ADD ()- GLBlendFuncSeparate GL_SRC_ALPHA GL_ONE_MINUS_SRC_ALPHA GL_ONE GL_ZERO ()-- GLEnable GL_DEPTH_TEST ()- GLDepthMask GL_FALSE ()- else do- GLDisable GL_BLEND ()-- GLEnable GL_DEPTH_TEST ()- GLDepthMask GL_TRUE ()- let (alphaFinish :: GLIOF ()) = do- if isAlpha- then do- GLDisable GL_BLEND ()-- GLDepthMask GL_TRUE ()- else do- GLDisable GL_BLEND ()-- GLDepthMask GL_TRUE ()-- -- Now aggregate the rendering to render the geom pass.- let (renderGeomPassGL :: GLIOF ()) = do- alphaSetup- assignTextures- renderGeomPassScene- alphaFinish- () <- monadic -< sdlGL1' renderGeomPassGL-- -- Return the state context.- let cxt = cxtnp2- returnA -< cxt-- where- -- | Add 2 variants: lookup sol mtrl for path, and also accumulate results in a list.- cachingRenderMtrlAccum :: Wire ImmutaballM ((LevelIB, Int32), ([((WidthHeightI, GLuint), MtrlMeta)], IBStateContext)) ([((WidthHeightI, GLuint), MtrlMeta)], IBStateContext)- cachingRenderMtrlAccum = proc ((sol, mi), (accum_, cxtn)) -> do- let mtrl = (sol^.solMv) ! mi- let mtrlPath = mtrl^.mtrlF- (glTextureMeta, cxtnp1) <- cachingRenderMtrl -< (mtrlPath, cxtn)- returnA -< (glTextureMeta:accum_, cxtnp1)
@@ -13,7 +13,9 @@ transformSol, restoreSolTransformation, restoreSolTransformationSimple,- mapcSolTransformationSimple+ mapcSolTransformationSimple,+ solErp,+ solErpdn ) where import Prelude ()@@ -72,3 +74,16 @@ (Vec3 1.0 0.0 0.0) (Vec3 0.0 0.0 1.0) (Vec3 0.0 (-1.0) 0.0)++-- | Convert a linear parameter (e.g. from 0 to 1) to a parameter that reflects+-- the sol ‘erp’ interpolation formula, for smooth paths.+solErp :: (Fractional a) => a -> a+solErp t = -2.0*t*t*t + 3.0*t*t++-- | Variant of 'solErp' that calculates derivatives an arbitrary number of times.+solErpdn :: (Fractional a) => Integer -> a -> a+solErpdn 0 t = -2.0*t*t*t + 3.0*t*t -- -2t^3 + 3t^2+solErpdn 1 t = -6.0*t*t + 6.0*t -- -6t^2 + 6t+solErpdn 2 t = -12.0*t + 6.0 -- -12t + 6+solErpdn 3 _t = -12.0 -- -12+solErpdn _ _t = 0
@@ -23,6 +23,7 @@ r2, t2, Vec3(..), x3, y3, z3,+ rv3, pv3, sv3, minusv3,@@ -113,6 +114,10 @@ v2orWith, v3or, v3orWith,+ v4or,+ v4orWith,+ flor,+ florWith, vx3, v2perp, v3perp,@@ -206,12 +211,12 @@ worldToGLSimple, rescaleDepth, - -- * 3D vector aiming rotation utils in radians: horizontal and vertical aiming of a point relative to origin.+ -- * 3D vector aiming rotation utils in radians: horizontal and vertical aiming of a point relative to origin aimHoriz3DSimple, aimVert3DSimple, - -- * More utils.+ -- * More utils v2z, v2s, v2nzElse,@@ -225,7 +230,7 @@ v4nzElse, v4nsElse, - -- * Subvectors.+ -- * Subvectors xy3, xz3, yz3,@@ -240,7 +245,13 @@ xzw4, yzw4, - sqx+ sqx,++ -- * Equivalence and sign utils+ thresholdSignnum,+ nearSignnum,+ thresholdSignnumI,+ nearSignnumI ) where import Prelude ()@@ -282,6 +293,9 @@ abs = fmap abs (*) = cm2 signum = fmap signum+instance (Num a, Fractional a) => Fractional (Vec2 a) where+ a / b = a * (recip <$> b)+ fromRational = rv2 . fromRational rv2 :: a -> Vec2 a rv2 z = Vec2 z z@@ -372,6 +386,9 @@ abs = fmap abs (*) = cm3 signum = fmap signum+instance (Num a, Fractional a) => Fractional (Vec3 a) where+ a / b = a * (recip <$> b)+ fromRational = rv3 . fromRational rv3 :: a -> Vec3 a rv3 z = Vec3 z z z@@ -448,6 +465,9 @@ abs = fmap abs (*) = cm4 signum = fmap signum+instance (Num a, Fractional a) => Fractional (Vec4 a) where+ a / b = a * (recip <$> b)+ fromRational = rv4 . fromRational rv4 :: a -> Vec4 a rv4 z = Vec4 z z z z@@ -1012,6 +1032,25 @@ | isNaN z || isInfinite z = else_ | otherwise = v +v4or :: (RealFloat a) => Vec4 a -> Vec4 a+v4or = v4orWith (Vec4 0.0 0.0 0.0 0.0)++v4orWith :: (RealFloat a) => Vec4 a -> Vec4 a -> Vec4 a+v4orWith else_ v@(Vec4 x y z w)+ | isNaN x || isInfinite x = else_+ | isNaN y || isInfinite y = else_+ | isNaN z || isInfinite z = else_+ | isNaN w || isInfinite w = else_+ | otherwise = v++flor :: (RealFloat a) => a -> a+flor = florWith 0.0++florWith :: (RealFloat a) => a -> a -> a+florWith else_ x+ | isNaN x || isInfinite x = else_+ | otherwise = x+ -- | Cross product. -- -- Satisfies a x b = |a| * |b| * sin(t) * n, providing a vector perpendicular@@ -1867,7 +1906,7 @@ where (do_, ds) = (depthTranslate, depthScale) --- * 3D vector aiming rotation utils in radians: horizontal and vertical aiming of a point relative to origin.+-- * 3D vector aiming rotation utils in radians: horizontal and vertical aiming of a point relative to origin -- | The vector represents where the camera at the origin is pointing. Rotate -- aim right by ‘radiansRight’ radians.@@ -1913,7 +1952,7 @@ Nothing -> radiansUp Just _ -> min maxRadiansUp . max minRadiansUp $ radiansUp --- * More utils.+-- * More utils -- | Is the vector zero? v2z :: (SmallNum a, Ord a, Num a, RealFloat a) => Vec2 a -> Bool@@ -1963,7 +2002,7 @@ v4nsElse :: (SmallishNum a, Ord a, Num a, RealFloat a) => Vec4 a -> Vec4 a -> Vec4 a v4nsElse v else_ = if' (not $ v4s v) v else_ --- * Subvectors.+-- * Subvectors -- | Handle just the ‘xy’ vector in a Vec3. xy3 :: forall a. Lens' (Vec3 a) (Vec2 a)@@ -2085,3 +2124,33 @@ -- | Convenient utility to square a number. sqx :: forall a. (Num a) => a -> a sqx x = x*x++-- * Equivalence and sign utils++-- | Determine if the number is zero, negative, or positive.+thresholdSignnum :: forall a i. (SmallNum a, Num a, Ord a, Num i) => a -> i+thresholdSignnum x+ | x `equivalentSmall` 0 = 0+ | x <= 0 = -1+ | otherwise = 1++-- | 'nearSignum' specialized to Integer.+nearSignnum :: forall a i. (SmallishNum a, Num a, Ord a, Num i) => a -> i+nearSignnum x+ | x `near` 0 = 0+ | x <= 0 = -1+ | otherwise = 1++-- | 'thresholdSignum' specialized to Integer.+thresholdSignnumI :: forall a. (SmallNum a, Num a, Ord a) => a -> Integer+thresholdSignnumI x+ | x `equivalentSmall` 0 = 0+ | x <= 0 = -1+ | otherwise = 1++-- | 'nearSignum' specialized to Integer.+nearSignnumI :: forall a. (SmallishNum a, Num a, Ord a) => a -> Integer+nearSignnumI x+ | x `near` 0 = 0+ | x <= 0 = -1+ | otherwise = 1
@@ -6,6 +6,7 @@ {-# LANGUAGE Haskell2010 #-} {-# LANGUAGE TemplateHaskell, DerivingVia, ScopedTypeVariables #-}+{-# LANGUAGE FlexibleInstances, UndecidableInstances #-} -- SmallishInfiniteLineThreshold -- | Dependent types might make for a funner linear algebra implementation, so -- I just stick with what's most applicable for our uses and goals here.@@ -37,12 +38,17 @@ line3AxisReflectPlane3, line3Lerp,+ smallishInfiniteLineThresholdd,+ smallishInfiniteLineThresholdf,+ SmallishInfiniteLineThreshold(..), line3CoordAtDistancePlane3,+ line3PlaneIntersection, line3PointCoord, line3PointDistance, line3DistanceCoordFromPoint, line3Line3ClosestCoords,+ line3Line3ClosestCoordsAny, line3Line3Distance, eqPlane3,@@ -55,6 +61,8 @@ nearPlane3PointsOnly, nearLine3PointsOnly, + plane3Plane3,+ QCurve3(..), a2q3, a1q3, a0q3, qcurve3, qcurvePath3,@@ -70,6 +78,7 @@ import Control.Lens import Immutaball.Share.Math.Core+import Immutaball.Share.Utils -- | Normal, distance. --@@ -326,6 +335,17 @@ line3Lerp :: forall a. (Num a) => Line3 a -> a -> Vec3 a line3Lerp l x = (l^.ol3) `pv3` (x `sv3` (l^.a0l3)) +smallishInfiniteLineThresholdd :: Double+smallishInfiniteLineThresholdd = 0.001++smallishInfiniteLineThresholdf :: Float+smallishInfiniteLineThresholdf = 0.001++class SmallishInfiniteLineThreshold a where smallishInfiniteLineThreshold :: a+instance {-# OVERLAPPING #-} SmallishInfiniteLineThreshold Double where smallishInfiniteLineThreshold = smallishInfiniteLineThresholdd+instance {-# OVERLAPPING #-} SmallishInfiniteLineThreshold Float where smallishInfiniteLineThreshold = smallishInfiniteLineThresholdf+instance {-# OVERLAPPABLE #-} (Fractional a) => SmallishInfiniteLineThreshold a where smallishInfiniteLineThreshold = realToFrac $ smallishInfiniteLineThresholdf+ -- | Given a plane, find the 1D coord that projects onto the line, that finds -- the point at the given distance from the plane. i.e. for a line and plane, -- find x such that p0 + p1*x gives a point equal to distance d from the plane@@ -334,6 +354,10 @@ -- -- Conveniently, the change in distance from the plane per change in x is -- constant.+--+-- Beware the sign of the distance.+--+-- This is better suited for lines with non-trivial length. See 'SmallishInfiniteLineThreshold'. line3CoordAtDistancePlane3 :: forall a. (SmallNum a, Ord a, Num a, Fractional a) => Plane3 a -> Line3 a -> a -> Maybe a line3CoordAtDistancePlane3 p l d | dd_dx `equivalentSmall` 0 = Nothing@@ -347,6 +371,12 @@ p0d = plane3PointDistance p (l^.p0l3) p1d = plane3PointDistance p (l^.p1l3) +-- | Find the point on the plane where an infinite line intersects.+--+-- This is better suited for lines with non-trivial length. See 'SmallishInfiniteLineThreshold'.+line3PlaneIntersection :: forall a. (SmallNum a, Ord a, Num a, Fractional a) => Line3 a -> Plane3 a -> Maybe (Vec3 a)+line3PlaneIntersection l p = line3Lerp l <$> line3CoordAtDistancePlane3 p l 0+ -- | Find the coord on the line to the closest point on that line to the given -- point in 3D space. --@@ -387,7 +417,8 @@ line3DistanceCoordFromPoint l v distance = (sqrt $ sqx distance - sqx (line3PointDistance l v)) / (l^.a0l3.r3) -- | Given lines la and lb, find coords ‘ax’ and ‘bx’ on la and on lb--- representing the points at which the two lines are closest to each other.+-- representing the points at which the two infinite lines are closest to each+-- other. -- -- (Paralleling the order, the first coord is on the first line, and the second -- coord is on the second line.)@@ -478,7 +509,23 @@ bx = line3PointCoord lb $ line3Lerp la ax --- | Find the (closest) distance between 2 lines.+-- | Like 'line3Line3ClosestCoords', but if the lines are parallel, such that+-- there are infinitely many close points, pick as an arbitrary point la at+-- coord ax=0, and lb at the closest point.+line3Line3ClosestCoordsAny :: forall a. (Show a, SmallNum a, Fractional a, RealFloat a) => Line3 a -> Line3 a -> (a, a)+line3Line3ClosestCoordsAny la lb+ | Just (ax, bx) <- line3Line3ClosestCoords la lb+ {-+ , not (isNaN ax || isInfinite ax)+ , not (isNaN bx || isInfinite bx) =+ -} =+ (ax, bx)+ | otherwise =+ let ax = 0.0 in+ let bx = line3PointCoord lb $ line3Lerp la ax in+ (ax, bx)++-- | Find the (closest) distance between 2 infinite lines. line3Line3Distance :: forall a. (Show a, SmallNum a, Fractional a, RealFloat a) => Line3 a -> Line3 a -> a line3Line3Distance la lb = case line3Line3ClosestCoords la lb of Nothing -> line3PointDistance la $ line3Lerp lb 0@@ -499,7 +546,8 @@ nearLine3 a b = (a^.p0l3) `near3` (b^.p0l3) && (a^.p1l3) `near3` (b^.p1l3) -- These equivalence variants check the points only; you can e.g. use--- ‘negatePlaneOrientation’ and preserve equivalence.+-- ‘negatePlaneOrientation’ and preserve equivalence. (That is, flipping the+-- plane normal doesn't necessarily change which points are on the plane.) eqPlane3PointsOnly :: (SmallNum a, Ord a, Num a, RealFloat a) => Plane3 a -> Plane3 a -> Bool eqPlane3PointsOnly a b = let a' = negatePlaneOrientation a in (a^.unPlane3) `eq4` (b^.unPlane3) || (a'^.unPlane3) `eq4` (b^.unPlane3)@@ -514,6 +562,28 @@ nearLine3PointsOnly :: (SmallishNum a, Ord a, Num a, RealFloat a) => Line3 a -> Line3 a -> Bool nearLine3PointsOnly a b = let na' = na & (a0l3 %~ negate) in ( (na^.p0l3) `near3` (nb^.p0l3) && (na^.p1l3) `near3` (nb^.p1l3) ) || ( (na'^.p0l3) `near3` (nb^.p0l3) && (na'^.p1l3) `near3` (nb^.p1l3) ) where (na, nb) = let standardize = (a0l3 %~ v3normalize) . line3NormalizeDisplacement in (standardize a, standardize b)++-- | Find the the line where 2 planes intersect, if they are not parallel.+plane3Plane3 :: (Num a, RealFloat a, Floating a, Ord a, SmallNum a) => Plane3 a -> Plane3 a -> Maybe (Line3 a)+plane3Plane3 pa pb = do+ -- Get a0, the axis of the line, from the cross product of the 2 plane+ -- normals.+ let a0Dir = (pa^.abcp3) `vx3` (pb^.abcp3)+ let a0DirUnit = v3or $ v3normalize a0Dir+ a0 <- if' (min (a0Dir^.r3) (a0DirUnit^.r3) <= smallNum) Nothing $ Just a0DirUnit++ -- To find any point where the planes intersect, take a point on pa, make a+ -- line from it using pa's normal cross a0, and find where this line+ -- intersects pb. Use this arbitrary origin as an arbitrary point on the+ -- line, and then normalize the displacement so that it's a common form.+ let pap = pointToPlane zv3 pa -- Closest point on pa to the origin.+ let lab = line3Axes pap ((pa^.abcp3) `vx3` a0) -- Line from pap to some point on the line.+ lp0 <- line3PlaneIntersection lab pb -- An arbitrary point where pa intersects pb.++ let lPrenormalized = line3Axes lp0 a0+ let l = line3NormalizeDisplacement lPrenormalized++ return $ l -- | The curve represented by p = 1/2 (x^2) a2 + x a1 + a0 -- (The 1/2 comes from integrating twice.)
@@ -6,6 +6,7 @@ {-# LANGUAGE Haskell2010 #-} {-# LANGUAGE TemplateHaskell, UndecidableInstances, DerivingVia #-}+{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-} -- For 'FakeEOS'. module Immutaball.Share.Utils (@@ -46,9 +47,22 @@ falseAsIntegral, deconsMaybe, - morElse+ morElse, --AssumeEOS,++ setMapFilter,+ steppingMean,++ FakeEOS(..), fakeEOS,++ modfl,++ uncurry3,+ listOthers,++ runListT,+ liftList ) where import Prelude ()@@ -59,8 +73,11 @@ import Data.Functor.Compose import Data.List import Data.Maybe+import qualified Data.Set as S import Control.Lens+import qualified Pipes as P+import qualified Pipes.Prelude as P -- | See F-algebras and catamorphisms for the idiom. newtype Fixed f = Fixed {_fixed :: f (Fixed f)}@@ -209,3 +226,54 @@ instance Ord (AssumeEOS a) where _ <= _ = True instance Show (AssumeEOS a) where show _ = "(AssumeEOS)" -}++setMapFilter :: (Ord a, Ord b) => (a -> Maybe b) -> S.Set a -> S.Set b+setMapFilter f s =+ S.map (\x -> case x of+ Nothing -> error "Internal error: setMapFilter found a Nothing after removing all Nothings."+ Just a -> a) .+ S.filter (\x -> case x of+ Nothing -> False+ Just _ -> True) .+ S.map f $+ s++-- | Find the mean value.+--+-- (lastLen*lastMean + x)/(lastLen + 1)+-- = (lastLen/(lastLen + 1))*lastMean + x/(lastLen + 1)+steppingMean :: (Foldable t, Num a, Fractional a) => t a -> a+steppingMean = snd . foldr (\x (lastLen, lastMean) -> let lastLenP1 = lastLen + 1 in (lastLenP1, (lastLen/lastLenP1)*lastMean + x/lastLenP1)) (0, 0)++-- | Considers all functions equal. Compiler record lookup obtains a different+-- record for Eq, Ord, Show instance values.+newtype FakeEOS a = FakeEOS { _fakeEOS :: a }+makeLenses ''FakeEOS+instance Eq (FakeEOS a) where _ == _ = True+instance Ord (FakeEOS a) where _ <= _ = True+instance Show (FakeEOS a) where show _ = "(FakeEOS)"++-- | 'mod' generalized to Double and floats.+modfl :: (RealFrac a) => a -> a -> a+modfl a b = a - b*(fromInteger . floor $ a/b)++-- | 3-ary 'uncurry'.+uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d+uncurry3 f (a, b, c) = f a b c++-- | Given e.g. a list [1,2,3], return [(1,[2,3]), (2,[1,3]), (3,[1,2])].+listOthers :: [a] -> [(a, [a])]+listOthers [] = []+listOthers (x:xs) = (x,xs) : fmap (second (x:)) (listOthers xs)++-- | Run a ListT monad.+--+-- ListT is provided by the ‘pipes’ package.+runListT :: (Monad m) => P.ListT m a -> m [a]+runListT (P.Select m) = P.toListM m++-- | Lift a plain list to a ListT monad.+--+-- ListT is provided by the ‘pipes’ package.+liftList :: (Functor m) => [a] -> P.ListT m a+liftList = P.Select . P.each
Main.hs view
@@ -1,6 +1,7 @@ #!/usr/bin/env -S bash -c 'eval "$(cat "$0" | head -n+3 | tail -n+3 | tail -c+3)" "$0" "${@@Q}"' {--#!/usr/bin/env -S runhaskell '-package containers' '-package bytestring' '-package transformers' '-package pipes' '-package lens' '-package parallel' '-package stm' '-package async' '-package wires' '-package mtl' '-package filepath' '-package directory' '-package prettyprinter' '-package parsec' '-package unbounded-delays' '-package time' '-package sdl2' '-package gl' '-package array' '-package OpenGL' '-package sdl2-ttf' '-package libvorbis' '-package JuicyPixels' '-package curl' '-package i18n' '-package text'+#!/usr/bin/env -S runhaskell '-i.:./dependency-substitutions/wires-nofallback-meta' '-package containers' '-package bytestring' '-package transformers' '-package pipes' '-package lens' '-package parallel' '-package stm' '-package async' '-package wires' '-package mtl' '-package filepath' '-package directory' '-package prettyprinter' '-package parsec' '-package unbounded-delays' '-package time' '-package sdl2' '-package gl' '-package array' '-package OpenGL' '-package sdl2-ttf' '-package libvorbis' '-package JuicyPixels' '-package curl' '-package i18n' '-package text'+#!/usr/bin/env -S runhaskell '-i.:./dependency-substitutions/wires-fallback-meta' '-package containers' '-package bytestring' '-package transformers' '-package pipes' '-package lens' '-package parallel' '-package stm' '-package async' '-package mtl' '-package filepath' '-package directory' '-package prettyprinter' '-package parsec' '-package unbounded-delays' '-package time' '-package sdl2' '-package gl' '-package array' '-package OpenGL' '-package sdl2-ttf' '-package libvorbis' '-package JuicyPixels' '-package curl' '-package i18n' '-package text' -} {-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces . -- Enable warnings:
+ NOTES-building-dependencies.md view
@@ -0,0 +1,97 @@+# Building dependencies++Immutaball now can provide its own fallback implementation if the wires+dependency fails to build, with its own internal rewrite of the FRP library.++As of this writing, the latest wires package on Hackage is out-of-date with the+most recent versions of its dependencies on Hackage, so it will fail to build+without local modifications, until a new, up-to-date version is submitted to+Hackage.++However, if you still wish to build wires locally so that Immutaball can use it+as an external dependency, here are my old build notes on changes that can get+it to build with up-to-date dependencies.++## Build notes++### wires 0.2.1++As of 2024-09-27, the latest wires dependency, v0.2.1, is a little out of date.+Simply building a new local version e.g. v0.2.1.0.1, with updated dependency+upper bounds with a new semialign dependency with the following changes is+sufficient to build wires.++Thus this package depends on wires > v0.2.1.++To ensure that wires has consistent dependencies with this package, I recommend+you copy the build-deps from this package and append them to your local wires+source package, and then manually install the local wires source package by+running ‘cabal install --lib’ inside your git clone (this adds to the user+cabal store and updates e.g. `~/.ghc/x86_64-linux-9.4.2/environments/default`),+with the changes mentioned, including increasing the upper bounds of ‘wires’'s+existing dependencies (see ‘New Dependencies’). Otherwise when building this+package, local packages such as your locally install ‘wires’ would be fixed to+the dependencies chosen when you built it, which may be incompatible with this+package's dependencies.++Old build tips:++(You may build it with ‘cabal install --lib --package-env=./package-env.txt’,+looking for the package-db path inside ./package-env.txt afterwards, and then+building immutaball with e.g.+‘cabal build --package-db=~/.local/state/cabal/store/ghc-9.4.2/package.db’)++(You may also consider manually installing all deps with ‘cabal install --lib+dep’ before building old deps like wires, to help it pick recent versions of+dependencies, without later failing to choose newer dependencies because wires+was earlier built with older dependencies and must be re-built. Alternatively,+comment out ‘wires’ from the .cabal file, run cabal build so it builds the+dependencies except for ‘wires’, and then uncomment ‘wires’.)++(Finally, adding a ‘text >= 2.1.1 && < 2.2’ version to ‘wires’ before installing+it with ‘cabal install --lib’ inside the git clone may help cabal build this+package. Additionally, you can even copy this package's dependencies and append them to+wires' dependencies to ensure the dependencies are consistent.)++#### Semialign change++In Control/Wires/Internal.hs, replace++```+instance Align Event where+ nil = NotNow+```++with++```+instance Align Event where+ nil = NotNow+instance Semialign Event where+```++#### Utils change++In Control/Wires/Utils.hs, replace++```+import Data.These+```++with++```+import Data.These.Combinators+```++#### New dependencies++```+base >= 4.8 && < 5,+deepseq >= 1.4.0 && < 1.7,+mtl >= 2.0 && < 5.7,+profunctors >= 5.0 && < 5.7,+semigroupoids >= 5.0 && < 6.1,+these >= 0.7.0 && < 1.3,+semialign >= 1.3.1 && < 1.4+```
Putt.hs view
@@ -1,6 +1,7 @@ #!/usr/bin/env -S bash -c 'eval "$(cat "$0" | head -n+3 | tail -n+3 | tail -c+3)" "$0" "${@@Q}"' {--#!/usr/bin/env -S runhaskell '-package containers' '-package bytestring' '-package transformers' '-package pipes' '-package lens' '-package parallel' '-package stm' '-package async' '-package wires' '-package mtl' '-package filepath' '-package directory' '-package prettyprinter' '-package parsec' '-package unbounded-delays' '-package time' '-package sdl2' '-package gl' '-package array' '-package OpenGL' '-package sdl2-ttf' '-package libvorbis' '-package JuicyPixels' '-package curl' '-package i18n' '-package text'+#!/usr/bin/env -S runhaskell '-i.:./dependency-substitutions/wires-nofallback-meta' '-package containers' '-package bytestring' '-package transformers' '-package pipes' '-package lens' '-package parallel' '-package stm' '-package async' '-package wires' '-package mtl' '-package filepath' '-package directory' '-package prettyprinter' '-package parsec' '-package unbounded-delays' '-package time' '-package sdl2' '-package gl' '-package array' '-package OpenGL' '-package sdl2-ttf' '-package libvorbis' '-package JuicyPixels' '-package curl' '-package i18n' '-package text'+#!/usr/bin/env -S runhaskell '-i.:./dependency-substitutions/wires-fallback-meta' '-package containers' '-package bytestring' '-package transformers' '-package pipes' '-package lens' '-package parallel' '-package stm' '-package async' '-package mtl' '-package filepath' '-package directory' '-package prettyprinter' '-package parsec' '-package unbounded-delays' '-package time' '-package sdl2' '-package gl' '-package array' '-package OpenGL' '-package sdl2-ttf' '-package libvorbis' '-package JuicyPixels' '-package curl' '-package i18n' '-package text' -} {-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces . -- Enable warnings:
README.md view
@@ -1,14 +1,10 @@ # Prototype Currently this project only implements basic parts of the game: minimal GUI,-basic physics, and a renderer. Much of the game is currently unimplemented-(audio, goals, moving bodies, gameplay mechanics, a more efficient physics-implementation), but this project still serves as a useful example of a purely-functional FRP application.--Currently the physics does not apply BSP partitioning but brute force checks-collisions with every body in the level each frame. Larger levels can't handle-this, but many of the levels in the first three level sets are playable.+basic physics, and a basic renderer. Much of the game is currently+unimplemented (audio, goals, complete moving body physics, gameplay mechanics,+etc.), but this project still serves as a useful example of a purely functional+FRP application. Currently the physics has a few bugs. # Immutaball @@ -21,95 +17,13 @@  -Demo video: <https://byronjohnson.net/immutaball/immutaball-v0.1.0.1-demo.html>--## Build notes--### wires 0.2.1--As of 2024-09-27, the latest wires dependency, v0.2.1, is a little out of date.-Simply building a new local version e.g. v0.2.1.0.1, with updated dependency-upper bounds with a new semialign dependency with the following changes is-sufficient to build wires.--Thus this package depends on wires > v0.2.1.--To ensure that wires has consistent dependencies with this package, I recommend-you copy the build-deps from this package and append them to your local wires-source package, and then manually install the local wires source package by-running ‘cabal install --lib’ inside your git clone (this adds to the user-cabal store and updates e.g. `~/.ghc/x86_64-linux-9.4.2/environments/default`),-with the changes mentioned, including increasing the upper bounds of ‘wires’'s-existing dependencies (see ‘New Dependencies’). Otherwise when building this-package, local packages such as your locally install ‘wires’ would be fixed to-the dependencies chosen when you built it, which may be incompatible with this-package's dependencies.--Old build tips:--(You may build it with ‘cabal install --lib --package-env=./package-env.txt’,-looking for the package-db path inside ./package-env.txt afterwards, and then-building immutaball with e.g.-‘cabal build --package-db=~/.local/state/cabal/store/ghc-9.4.2/package.db’)--(You may also consider manually installing all deps with ‘cabal install --lib-dep’ before building old deps like wires, to help it pick recent versions of-dependencies, without later failing to choose newer dependencies because wires-was earlier built with older dependencies and must be re-built. Alternatively,-comment out ‘wires’ from the .cabal file, run cabal build so it builds the-dependencies except for ‘wires’, and then uncomment ‘wires’.)--(Finally, adding a ‘text >= 2.1.1 && < 2.2’ version to ‘wires’ before installing-it with ‘cabal install --lib’ inside the git clone may help cabal build this-package. Additionally, you can even copy this package's dependencies and append them to-wires' dependencies to ensure the dependencies are consistent.)--#### Semialign change--In Control/Wires/Internal.hs, replace--```-instance Align Event where- nil = NotNow-```--with--```-instance Align Event where- nil = NotNow-instance Semialign Event where-```--#### Utils change--In Control/Wires/Utils.hs, replace--```-import Data.These-```--with--```-import Data.These.Combinators-```--#### New dependencies--```-base >= 4.8 && < 5,-deepseq >= 1.4.0 && < 1.7,-mtl >= 2.0 && < 5.7,-profunctors >= 5.0 && < 5.7,-semigroupoids >= 5.0 && < 6.1,-these >= 0.7.0 && < 1.3,-semialign >= 1.3.1 && < 1.4-```+Demo video: <https://byronjohnson.net/immutaball/immutaball-v0.1.0.1-demo.html>.+(Demo recorded before BSP physics algorithm.) ## Usage example ```-(cd -- "${HOME}/git/neverball" && make -j7) # build neverball-cabal run --package-db="${HOME}/.local/state/cabal/store/ghc-9.13.20240927/package.db" immutaball -- -d ~/git/neverball/data+(cd -- "${HOME}/git" && git clone https://github.com/Neverball/neverball) # clone neverball+(cd -- "${HOME}/git/neverball" && make -j7) # build neverball+cabal run immutaball -- -d ~/git/neverball/data # run immutaball ```
Test.hs view
@@ -1,6 +1,7 @@ #!/usr/bin/env -S bash -c 'eval "$(cat "$0" | head -n+3 | tail -n+3 | tail -c+3)" "$0" "${@@Q}"' {--#!/usr/bin/env -S runhaskell '-package containers' '-package bytestring' '-package transformers' '-package pipes' '-package lens' '-package parallel' '-package stm' '-package async' '-package wires' '-package mtl' '-package filepath' '-package directory' '-package prettyprinter' '-package parsec' '-package unbounded-delays' '-package time' '-package sdl2' '-package gl' '-package array' '-package OpenGL' '-package sdl2-ttf' '-package libvorbis' '-package JuicyPixels' '-package curl' '-package i18n' '-package text' '-package tasty' '-package tasty-hunit' '-package tasty-quickcheck' '-package HUnit' '-package QuickCheck'+#!/usr/bin/env -S runhaskell '-i.:./dependency-substitutions/wires-nofallback-meta' '-package containers' '-package bytestring' '-package transformers' '-package pipes' '-package lens' '-package parallel' '-package stm' '-package async' '-package wires' '-package mtl' '-package filepath' '-package directory' '-package prettyprinter' '-package parsec' '-package unbounded-delays' '-package time' '-package sdl2' '-package gl' '-package array' '-package OpenGL' '-package sdl2-ttf' '-package libvorbis' '-package JuicyPixels' '-package curl' '-package i18n' '-package text' '-package tasty' '-package tasty-hunit' '-package tasty-quickcheck' '-package HUnit' '-package QuickCheck'+#!/usr/bin/env -S runhaskell '-i.:./dependency-substitutions/wires-fallback-meta' '-package containers' '-package bytestring' '-package transformers' '-package pipes' '-package lens' '-package parallel' '-package stm' '-package async' '-package mtl' '-package filepath' '-package directory' '-package prettyprinter' '-package parsec' '-package unbounded-delays' '-package time' '-package sdl2' '-package gl' '-package array' '-package OpenGL' '-package sdl2-ttf' '-package libvorbis' '-package JuicyPixels' '-package curl' '-package i18n' '-package text' '-package tasty' '-package tasty-hunit' '-package tasty-quickcheck' '-package HUnit' '-package QuickCheck' -} {-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces . -- Enable warnings:
+ Test/Data/LabeledBinTree/Orphans.hs view
@@ -0,0 +1,45 @@+{-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .+-- Enable warnings:+{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}++-- Orphans.hs.++{-# LANGUAGE Haskell2010 #-}+{-# LANGUAGE TemplateHaskell #-}++{-# OPTIONS_GHC -Wno-orphans #-}++module Test.Data.LabeledBinTree.Orphans+ (+ ) where++import Prelude ()+import Immutaball.Prelude++import Data.Function hiding (id, (.))++import Test.QuickCheck++import Data.LabeledBinTree+import Immutaball.Share.Utils++instance (Arbitrary a) => Arbitrary (LabeledBinTree a) where+ -- | Styled as recommended in the QuickCheck manual.+ arbitrary = sized . fix $ \tree -> \size -> let subtree = tree (size `div` 2) in+ if' (size <= 0) (pure emptyLBT) .+ if' (size == 1) (leafLBT <$> arbitrary) .+ oneof $ [+ pure emptyLBT,+ leafLBT <$> arbitrary,+ forkLBT <$> subtree <*> arbitrary <*> subtree+ ]+ shrink = deconsLabeledBinTree+ []+ (\_a -> [emptyLBT])+ (\l a r -> concat+ [+ [emptyLBT],+ [l, r],+ [forkLBT l' a' r' | (l', a', r') <- shrink (l, a, r)]+ ]+ )
+ Test/Data/LabeledBinTree/Test.hs view
@@ -0,0 +1,79 @@+{-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .+-- Enable warnings:+{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}++-- Test.hs.++{-# LANGUAGE Haskell2010 #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Test.Data.LabeledBinTree.Test+ (+ main,+ testsMain,+ tests,++ simpleConstant+ ) where++--import Control.Arrow+--import Data.Functor.Identity++--import Control.Lens+import Control.Monad+import Test.HUnit+--import Test.QuickCheck+import Test.Tasty+import Test.Tasty.HUnit hiding ((@?=), assertBool)+import Test.Tasty.QuickCheck++import Data.LabeledBinTree+import Test.Data.LabeledBinTree.Orphans ()++main :: IO ()+main = testsMain++testsMain :: IO ()+testsMain = defaultMain tests++simpleConstant :: Integer+simpleConstant = 3++tests :: TestTree+tests = testGroup "Data.LabeledBinTree" $+ [+ testCase "simpleConstant == 3" $+ simpleConstant @?= 3,++ -- I think it grows exponentionally (although I haven't rigorously+ -- worked out the tight big theta growth class for space if everything+ -- were strictly evaluated), so when testing for associativity, we'll+ -- want to limit the size of trees, since we're not just taking a+ -- single path or a select set of paths. e.g. I saw maxTreeSizeSum of+ -- 3*8 (average size of 8) result in a tree size of 443,621.+ testGroup "testing monadic associativity of Tree" $+ let maxTreeSizeSum = 3*6 in -- See above; probably exponential growth, and we're not traversing a single path but the entire tree.+ [+ testProperty "monadic associativity test of Tree 0" $+ \(int :: Integer) ->+ \(fints :: LabeledBinTree Integer) ->+ \(gints :: LabeledBinTree Integer) ->+ \(hints :: LabeledBinTree Integer) ->+ (sum $ numAnyDirectLBTI <$> [fints, gints, hints]) <= maxTreeSizeSum ==> -- This seems to stop what might be intermittent size explotions.+ let f = \y -> (\x -> x + y) <$> fints in+ let g = \y -> (\x -> x + y) <$> gints in+ let h = \y -> (\x -> x + y) <$> hints in+ ( (f <=< g) <=< h ) int `eqLBT` ( f <=< (g <=< h) ) int,++ testProperty "monadic associativity test of Tree 0, but with strict equality" $+ \(int :: Integer) ->+ \(fints :: LabeledBinTree Integer) ->+ \(gints :: LabeledBinTree Integer) ->+ \(hints :: LabeledBinTree Integer) ->+ (sum $ numAnyDirectLBTI <$> [fints, gints, hints]) <= maxTreeSizeSum ==> -- This seems to stop what might be intermittent size explotions.+ let f = \y -> (\x -> x + y) <$> fints in+ let g = \y -> (\x -> x + y) <$> gints in+ let h = \y -> (\x -> x + y) <$> hints in+ ( (f <=< g) <=< h ) int == ( f <=< (g <=< h) ) int+ ]+ ]
@@ -5,6 +5,7 @@ -- Test.hs. {-# LANGUAGE Haskell2010 #-}+{-# LANGUAGE ScopedTypeVariables #-} module Test.Immutaball.Share.Math.X3D.Test (@@ -15,7 +16,8 @@ simpleConstant, sampleLine0, sampleLine1,- planeX1+ planeX1,+ planeZ2 ) where --import Control.Arrow@@ -26,9 +28,10 @@ --import Test.QuickCheck import Test.Tasty import Test.Tasty.HUnit hiding ((@?=), assertBool)---import Test.Tasty.QuickCheck+import Test.Tasty.QuickCheck import Immutaball.Share.Math+import Immutaball.Share.Utils import Test.Immutaball.Share.Math.Core.Orphans () main :: IO ()@@ -49,6 +52,9 @@ planeX1 :: Plane3 Double planeX1 = normalPlane3 (Vec3 1 0 0) 1 +planeZ2 :: Plane3 Double+planeZ2 = normalPlane3 (Vec3 0 0 1) 2+ tests :: TestTree tests = testGroup "Immutaball.Share.Math.X3D" $ [@@ -64,9 +70,31 @@ testGroup "pointToPlane" $ [ testCase "simple sample test" $- (Vec3 7 (-4) 88 `pointToPlane` planeX1) `eq3` Vec3 1 (-4) 88 @?= True+ (Vec3 7 (-4) 88 `pointToPlane` planeX1) `eq3` Vec3 1 (-4) 88 @?= True,+ testProperty "the closest point on a random plane to the origin matches its normal and distance" $+ -- Get a random plane.+ \(pabcRaw :: Vec3 Double) ->+ \(pd :: Double ) ->+ let pabc = v3normalize pabcRaw & \v -> if' (not $ (v^.r3) `near` 1.0) (Vec3 1.0 0.0 0.0) $ v in+ let plane = normalPlane3 pabc pd in++ let p = pointToPlane zv3 plane in+ p `near3` ((plane^.dp3) `sv3` (plane^.abcp3)) ], + testGroup "line3PlaneIntersection" $+ [+ testProperty "a random plane's normal intersects the plane at the closest point on the plane to the origin" $+ -- Get a random plane.+ \(pabcRaw :: Vec3 Double) ->+ \(pd :: Double ) ->+ let pabc = v3normalize pabcRaw & \v -> if' (not $ (v^.r3) `near` 1.0) (Vec3 1.0 0.0 0.0) $ v in+ let plane = normalPlane3 pabc pd in++ let p = pointToPlane zv3 plane in+ (pure near3 <*> line3PlaneIntersection (line3Axes zv3 (plane^.abcp3)) plane <*> pure p) == Just True+ ],+ testGroup "plane3ReflectPoint" $ [ testCase "simple sample test" $@@ -130,5 +158,41 @@ line3Line3Distance sampleLine0 sampleLine1 `equivalentSmall` 1 @?= True, testCase "sample lines are distance 1 with second z-negated" $ line3Line3Distance sampleLine0 (sampleLine1 & ol3.z3 %~ negate) `equivalentSmall` 1 @?= True+ ],++ testGroup "plane3 plane3 tests" $+ [+ testCase "planeX1 planeZ2 intersects at" $+ let allowOtherDirection = False in+ ((pure nearLine3 <*> plane3Plane3 planeX1 planeZ2 <*> pure (line3Points (Vec3 1 0 2) (Vec3 1 (-1) 2))) == Just True) ||+ ((pure nearLine3 <*> plane3Plane3 planeX1 planeZ2 <*> pure (line3Points (Vec3 1 0 2) (Vec3 1 1 2))) == Just True && allowOtherDirection) @?= True,+ testProperty "planes intersect with points on both planes" $+ -- Get 2 random planes, pa and pb.+ \(paabcRaw :: Vec3 Double) ->+ \(pbabcRaw :: Vec3 Double) ->+ \(pad :: Double ) ->+ \(pbd :: Double ) ->+ \(lx :: Double ) ->+ let paabc = v3normalize paabcRaw & \v -> if' (not $ (v^.r3) `near` 1.0) (Vec3 1.0 0.0 0.0) $ v in+ let pbabc = v3normalize pbabcRaw & \v -> if' (not $ (v^.r3) `near` 1.0) (Vec3 1.0 0.0 0.0) $ v in+ let pa = normalPlane3 paabc pad in+ let pb = normalPlane3 pbabc pbd in++ -- Get their intersection.+ let ml = plane3Plane3 pa pb in+ case ml of+ Nothing ->+ -- They don't intersect; make sure the cross+ -- product of the 2 normals are smallish.+ let cross = (pa^.abcp3) `vx3` (pb^.abcp3) in+ cross^.r3 <= smallishNum+ Just l ->+ -- Get a random point on the intersection line.+ let p = line3Lerp l lx in++ -- Make sure it's on pa.+ plane3PointDistance pa p `near` 0 &&+ -- Make sure it's on pb.+ plane3PointDistance pb p `near` 0 ] ]
Test/Immutaball/Test.hs view
@@ -19,6 +19,7 @@ --import Test.Tasty.HUnit hiding ((@?=), assertBool) --import Test.Tasty.QuickCheck +import qualified Test.Data.LabeledBinTree.Test import qualified Test.Immutaball.Share.Math.Test import qualified Test.Immutaball.Share.State.Test import qualified Test.Immutaball.Share.Wire.Test@@ -32,6 +33,7 @@ tests :: Bool -> TestTree tests headless = testGroup "Immutaball" $ [+ Test.Data.LabeledBinTree.Test.tests, Test.Immutaball.Share.Wire.Test.tests, Test.Immutaball.Share.State.Test.tests headless, Test.Immutaball.Share.Math.Test.tests
+ dependency-substitutions/wires-fallback-meta/Control/Wire/Meta.hs view
@@ -0,0 +1,18 @@+{-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .+-- Enable warnings:+{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}++-- Wire.hs.++{-# LANGUAGE Haskell2010 #-}++-- | Information about which Wire implementation is used.+module Control.Wire.Meta+ (+ isFallbackWire+ ) where++-- | Is the fallback implementation used rather than the external ‘wires’+-- dependency?+isFallbackWire :: Bool+isFallbackWire = True
+ dependency-substitutions/wires-nofallback-meta/Control/Wire/Meta.hs view
@@ -0,0 +1,18 @@+{-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .+-- Enable warnings:+{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}++-- Wire.hs.++{-# LANGUAGE Haskell2010 #-}++-- | Information about which Wire implementation is used.+module Control.Wire.Meta+ (+ isFallbackWire+ ) where++-- | Is the fallback implementation used rather than the external ‘wires’+-- dependency?+isFallbackWire :: Bool+isFallbackWire = False
+ dependency-substitutions/wires/Control/Wire.hs view
@@ -0,0 +1,17 @@+{-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .+-- Enable warnings:+{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}++-- Wire.hs.++{-# LANGUAGE Haskell2010 #-}++-- | A rewrite of wires, sufficient for our purposes.+module Control.Wire+ (+ module Control.Wire.Controller,+ module Control.Wire.Internal+ ) where++import Control.Wire.Controller+import Control.Wire.Internal
+ dependency-substitutions/wires/Control/Wire/Controller.hs view
@@ -0,0 +1,15 @@+{-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .+-- Enable warnings:+{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}++-- Wire/Conttroller.hs.++{-# LANGUAGE Haskell2010 #-}++-- | A rewrite of wires, sufficient for our purposes.+module Control.Wire.Controller+ (+ module Control.Wire.Internal+ ) where++import Control.Wire.Internal
+ dependency-substitutions/wires/Control/Wire/Internal.hs view
@@ -0,0 +1,62 @@+{-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .+-- Enable warnings:+{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}++-- Wire/Internal.hs.++{-# LANGUAGE Haskell2010 #-}+--{-# LANGUAGE TemplateHaskell, Arrows, DerivingVia, RankNTypes #-}++-- | A rewrite of wires, sufficient for our purposes.+module Control.Wire.Internal+ (+ Wire(..),+ stepWire+ ) where++import Control.Arrow+import Control.Category as C+import Control.Monad.Fix++-- | A self-modifying function.+--+-- This is the central construct in the ‘wires’ FRP implementation.+newtype Wire m a b = Wire { _wireStep :: a -> m (b, Wire m a b) }++-- | Step a wire. Obtain its result and the next version of itself.+{-# INLINE stepWire #-}+stepWire :: Wire m a b -> a -> m (b, Wire m a b)+stepWire = _wireStep++instance (Functor m) => Functor (Wire m a) where+ {-# INLINE fmap #-}+ fmap f (Wire w) = Wire $ \a -> (\(b, w') -> (f b, fmap f w')) <$> w a++instance (Applicative m) => Applicative (Wire m i) where+ {-# INLINE pure #-}+ pure a = fix $ \me -> Wire $ \_i -> pure (a, me)+ {-# INLINE (<*>) #-}+ (Wire wf) <*> (Wire wa) = Wire $ \i -> pure (\(f, wf') (a, wa') -> (f a, wf' <*> wa')) <*> wf i <*> wa i++instance (Monad m) => Category (Wire m) where+ {-# INLINE id #-}+ id = fix $ \me -> Wire $ \a -> return (a, me)+ {-# INLINE (.) #-}+ (Wire wc) . (Wire wb) = Wire $ \a -> wb a >>= \(b, wb') -> wc b >>= \(c, wc') -> return (c, wc' C.. wb')++instance (Monad m) => Arrow (Wire m) where+ {-# INLINE arr #-}+ arr f = fix $ \me -> Wire $ \a -> pure (f a, me)+ {-# INLINE first #-}+ first (Wire w) = Wire $ \(a, d) -> (\(b, w') -> ((b, d), first w')) <$> w a++instance (Monad m) => ArrowChoice (Wire m) where+ -- (Tricky: in the Left case, don't use ‘me’ which would discard ‘w'’.)+ {-# INLINE left #-}+ left (Wire w) = fix $ \me -> Wire $ \bd -> either (\b -> (\(c, w') -> (Left c, left w')) <$> w b) (\d -> pure (Right d, me)) bd++instance (MonadFix m) => ArrowLoop (Wire m) where+ -- w :: (b, d) -> m ((c, d), Wire m (b, d) (c, d))+ -- Make sure this is lazy enough, or it could hang!+ {-# INLINE loop #-}+ loop (Wire w) = Wire $ \b -> (\((c, _d), w') -> (c, loop w')) <$> (mfix $ \ ~(~(_c, d), _w') -> w (b, d))
immutaball-core.cabal view
@@ -1,11 +1,12 @@ cabal-version: 3.0 name: immutaball-core-version: 0.1.0.4.1+version: 0.1.0.5.1 -synopsis: Immutaball platformer game+synopsis: Immutaball platformer game (prototype version) description: The Immutaball platformer game is a rewrite of Neverball in Haskell.+ (Prototype version.) category: Games homepage: https://nibzdil.org/ bug-reports: https://nibzdil.org/@@ -17,11 +18,13 @@ author: Byron Johnson maintainer: bjohnson@nibzdil.org build-type: Simple-copyright: 2024, Byron Johnson+copyright: 2024-2025, Byron Johnson extra-source-files: README.md+ NOTES-building-dependencies.md extra-doc-files:+ CHANGELOG.md doc/legal/*.md doc/legal/*.txt doc/screenshots/*.png@@ -30,10 +33,10 @@ type: git location: https://github.com/nibzdil/immutaball -source-repository this- type: git- location: https://github.com/nibzdil/immutaball- tag: v0.1.0.4.1-hackage+--source-repository this+-- type: git+-- location: https://github.com/nibzdil/immutaball+-- tag: v0.1.0.0-dev flag add-default-compiler-flags description:@@ -41,6 +44,16 @@ default: True manual: True +flag internal-wires+ description:+ Instead of an external wires dependency, use an internal implementation+ sufficient for this package. Useful since as of now, the latest+ version on Hackage is not up-to-date with the latest versions of its+ dependencies, so it would not build with this project without a+ manually modified local build of this external dependency.+ default: False+ manual: False+ common all-components default-language: Haskell2010 other-extensions:@@ -85,7 +98,7 @@ --stm >= 2.5.3.1 && < 2.6, stm >= 2.5.1.0 && < 2.6, async >= 2.2.5 && < 2.3,- wires > 0.2.1 && < 0.3,+ --wires > 0.2.1 && < 0.3, mtl >= 2.2 && < 2.4, --filepath >= 1.5.3.0 && < 1.6, filepath >= 1.4.2.2 && < 1.6,@@ -107,14 +120,37 @@ i18n >= 0.4.0.0 && < 0.5, text >= 2.1 && < 2.2 + if !flag(internal-wires)+ build-depends:+ wires > 0.2.1 && < 0.3+ hs-source-dirs:+ .+ ./dependency-substitutions/wires-nofallback-meta+ other-modules:+ Control.Wire.Meta+ else+ hs-source-dirs:+ .+ ./dependency-substitutions/wires-fallback-meta+ ./dependency-substitutions/wires+ other-modules:+ Control.Wire.Meta+ other-modules:+ Control.Wire+ Control.Wire.Controller+ Control.Wire.Internal+ common executable-components build-depends: immutaball-core other-modules: Control.Monad.Trans.MaybeM+ Data.LabeledBinTree Immutaball.Ball.CLI Immutaball.Ball.CLI.Config Immutaball.Ball.Game+ Immutaball.Ball.Level+ Immutaball.Ball.Level.Render Immutaball.Ball.LevelSets Immutaball.Ball.Main Immutaball.Ball.State.Game@@ -148,7 +184,6 @@ Immutaball.Share.Level.Analysis.LowLevel Immutaball.Share.Level.Attributes Immutaball.Share.Level.Base- Immutaball.Share.Level.Render Immutaball.Share.Level.Parser Immutaball.Share.Level.Utils Immutaball.Share.Math@@ -182,9 +217,12 @@ exposed-modules: Control.Monad.Trans.MaybeM+ Data.LabeledBinTree Immutaball.Ball.CLI Immutaball.Ball.CLI.Config Immutaball.Ball.Game+ Immutaball.Ball.Level+ Immutaball.Ball.Level.Render Immutaball.Ball.LevelSets Immutaball.Ball.Main Immutaball.Ball.State.Game@@ -218,7 +256,6 @@ Immutaball.Share.Level.Analysis.LowLevel Immutaball.Share.Level.Attributes Immutaball.Share.Level.Base- Immutaball.Share.Level.Render Immutaball.Share.Level.Parser Immutaball.Share.Level.Utils Immutaball.Share.Math@@ -253,6 +290,8 @@ QuickCheck >= 2.15.0.1 && < 2.16 other-modules:+ Test.Data.LabeledBinTree.Orphans+ Test.Data.LabeledBinTree.Test Test.Immutaball.Test Test.Immutaball.Share.Math.Core.Orphans Test.Immutaball.Share.Math.Core.Test