/* https://www.harrygodden.com HT8B v1.8.0 Update log: 16.12.2020 (0.1.3a) - Fix for new game, wrong local turn colour - Fix for not losing match when scratch on pot 8 ball ( Thanks: Photographotter, Mystical ) - Added permission info to console when fail reset 17.12.2020 (0.2.0a) - Predictive physics for cue ball - Fix for not winning when sink 8, and objective on same turn ( Thanks: Rosy ) - Reduced code spaghet in decode routine - Improved algorithm for post-game state checking, should lend to easier implementation of optional rules. - Allow colour switching between UK/USA/Default colour sets - Grips change colour based on which turn it is 0.3.0a - Added desktop mode - Sink opponents ball = loss turn - Removed coloursets 0.3.1a - Desktop QOL 0.3.2a - Reduced sensitivity - Added pad bytes 0.3.7a - Quest support 0.3.8a - Switched network string to base64 encoded - Changed initial break setup 1.0.0 - First full Release 1.5.0 - Physics rework to be more accurate (Marlow implementation) - Menu remade to be more buttony 1.5.3 - Physics patches, misc bug fixes 1.5.4 - Winner ID being set incorrectly due to menu fixed 1.5.41 - Fix for quest fail to build (thanks MrDummyNL, Legoman99573) 1.5.42 - Corrected cue colliding function for 4 ball mode - Removed / cleaned unused preprocessor directives - Fixed buttons that are disabled from being processed - Fix for timer being incorrectly labelled - Static resolution - Unlicense everything - Rename _frp to _log and similar 1.6.0 - Edge colliders use veroni regions and minkowski difference to detect and apply collisions - Removed atan2 usage in mobile shaders - Upgraded pocket triggers to be circles - Fixed issue with desktop mode breaking due to script order - Added dynamic cubemap that refreshes on newgame - Updated table geometry 1.6.2 - Fix for error on regions Ma, Mc for z(B) > z(A) causing warp ( Thanks: Traediras ) 1.6.3 - Removed check that would prevent error recovery 18.03.2021 1.7.0a - Use config string for collision data / colours - Misc bugs (improved in-editor ux) Networking Model Information: When a turn ends, the player who is currently playing will pack information into the networking string that the turn has been transferred, and once the remote client who is associated with the opposite cue recieves the update, they will take ownership of the main script. The local player will have a 'permit' to shoot when it is their turn, which allows them to interact with the physics world. As soon as the cue ball is shot, the script calculates and compresses the necessery velocities and positions of the balls, and 1. sends that out to remote clients, and 2. decodes it the same way themselves. So effectively all players end up watching the exact same simulation at very close to the same time. In testing this was immediate as it could be with a GB -> USA connection. Information about the data: - Data is transfered using 1 Udon Synced string which is 82 bytes long, encoded to base64( 110 bytes ) - Critical game states are packed into a bitmask at #19 - Floating point positions are encoded/decoded as follows: Encode: Divide the value by the expected maximum range Multiply that by signed short max value ~32k Add signed short max Cast to ushort Decode: Cast ushort to float Subtract short max Divide by short max Multiply by the same range encoded with - Ball ID's are designed around bitmasks and are as follows: byte | Byte 0 | Byte 1 | bit | x80 . x40 . x20 . x10 . x08 . x04 . x02 | x1 .. x80 . x40 . x20 . x10 . x08 . x04 | x02 | x01 | ball | 15 14 13 12 11 10 9 | 7 6 5 4 3 2 1 | 8 | cue | Networking Layout: Total size: 78 bytes over network // 39 C# wchar Address What Data type [ 0x00 ] ball positions (compressed quantized vec2's) [ 0x40 ] cue ball velocity ^ [ 0x44 ] cue ball angular vel ^ [ 0x4A ] sn_pocketed uint16 bitmask ( above table ) OR sn_gmspec | bit # | mask | what | | 0-3 | 0x0f | fb_scores[ 0 ] | | 4-7 | 0xf0 | fb_scores[ 1 ] | [ 0x4C ] game state flags | bit # | mask | what | | 0 | 0x1 | sn_simulating | | 1 | 0x2 | sn_turnid | | 2 | 0x4 | sn_foul | | 3 | 0x8 | sn_open | | 4 | 0x10 | sn_playerxor | | 5 | 0x20 | sn_gameover | | 6 | 0x40 | sn_winnerid | | 7 | 0x80 | sn_permit | | 8-10 | 0x700 | sn_gamemode | | 11 | 0x800 | sn_lobbyopen | | 12 | 0x1000 | | | 13-14 | 0x6000 | sn_timer | | 15 | 0x8000 | sn_allowteams | [ 0x4E ] packet # uint16 [ 0x50 ] gameid uint16 Physics Implementation: The implementation is designed to be numerically stable (eg. using linear algebra as much as possible to be explicit about what and where stuff collides ). Ball physic response is 100% pure elastic energy transfer, which even at one iteration per physics update seems to give plausable enough results. Edge collisions are a little contrived and the reason why the table can ONLY be placed at world orign. the table is divided into major and minor sections. some of the calculations can be peeked at here: https://www.geogebra.org/m/jcteyvj6 . It is all straight line equations. There MAY be deviations between SOME client cpus / platforms depending on the floating point architecture, and who knows what the fuck C# will decide to do at runtime anyway. However after some testing this seems rare enough that we could not observe any differences at all. If it does happen to be calculated differently, the remote clients will catch up with the players game anyway. I reckon this is most likely going to affect, if it does at all, only quest/pc crossplay and not much else. Physics are calculated on a fixed timestep, using accumulator model. If there is very low framerate physics may run at a slower timescale if it passes the threshold where maximum updates/frame is reached, but won't affect eventual outcome. Limited CCD is used on the cueball only, I do not know how to implement a variable timestep to account for every ball, especially on slow execution time (udon) live: wrld_d02883d1-7d79-4d51-87e2-b318ca4c2b37 dev: wrld_9497c2da-97ee-4b2e-9f82-f9adb024b6fe */ // https://feedback.vrchat.com/feature-requests/p/udon-expose-shaderpropertytoid // #define USE_INT_UNIFORMS #if !UNITY_ANDROID //#define HT8B_DEBUGGER #else #define HT_QUEST #endif using UdonSharp; using UnityEngine; using UnityEngine.UI; using VRC.SDKBase; using VRC.Udon; using System; [DefaultExecutionOrder(129)] public class ht8b : UdonSharpBehaviour { #region R_CONSTANTS #if HT_QUEST const float k_MAX_DELTA = 0.075f; // Maximum steps/frame ( 5 ish ) #else const float k_MAX_DELTA = 0.1f; // Maximum steps/frame ( 8 ) #endif // Physics calculation constants (measurements are in meters) // CONFIGURATION VALUES [SerializeField] [HideInInspector] public float k_TABLE_WIDTH; // horizontal span of table (v1.6: 1.054) [SerializeField] [HideInInspector] public float k_TABLE_HEIGHT; // vertical span of table (v1.6: 0.605) [SerializeField] [HideInInspector] public float k_CUSHION_RADIUS; // The roundess of colliders (v1.6: 0.043) [SerializeField] [HideInInspector] public float k_POCKET_RADIUS; // Full diameter of pockets (v1.6: 0.100) [SerializeField] [HideInInspector] public float k_INNER_RADIUS; // Pocket 'hitbox' cylinder (v1.6: 0.072) // Global [SerializeField] [HideInInspector] public Color k_colour_foul; // v1.6: ( 1.2, 0.0, 0.0, 1.0 ) [SerializeField] [HideInInspector] public Color k_colour_default; // v1.6: ( 1.0, 1.0, 1.0, 1.0 ) Color k_colour_off = new Color(0.01f, 0.01f, 0.01f, 1.0f); // 8 ball teams [SerializeField] [HideInInspector] public Color k_teamColour_spots; // v1.6: ( 0.00, 0.75, 1.75, 1.0 ) [SerializeField] [HideInInspector] public Color k_teamColour_stripes; // v1.6: ( 1.75, 0.25, 0.00, 1.0 ) // 4 Ball teams [SerializeField] [HideInInspector] public Color k_colour4Ball_team_0; // v1.6: ( ) [SerializeField] [HideInInspector] public Color k_colour4Ball_team_1; // v1.6: ( 2.0, 1.0, 0.0, 1.0 ) // Fabrics colours [SerializeField] [HideInInspector] public Color k_fabricColour_8ball; // v1.6: ( 0.3, 0.3, 0.3, 1.0 ) [SerializeField] [HideInInspector] public Color k_fabricColour_9ball; // v1.6: ( 0.1, 0.6, 1.0, 1.0 ) [SerializeField] [HideInInspector] public Color k_fabricColour_4ball; // v1.6: ( 0.15, 0.75, 0.3, 1.0 ) // pocket colliders [SerializeField] [HideInInspector] public Vector3 k_vE; // v1.6: ( 1.087, 0.0, 0.627 ) [SerializeField] [HideInInspector] public Vector3 k_vF; // v1.6: ( 0.000, 0.0, 0.665 ) // Materials [SerializeField] [HideInInspector] public Material ballMaterial; [SerializeField] public Material tableMaterial; [SerializeField] [HideInInspector] public Texture[] textureSets; const float k_FIXED_TIME_STEP = 0.0125f; // time step in seconds per iteration const float k_FIXED_SUBSTEP = 0.00125f; const float k_TIME_ALPHA = 50.0f; // (unused) physics interpolation const float k_BALL_DIAMETRE = 0.06f; // width of ball const float k_BALL_PL_X = 0.03f; // break placement X const float k_BALL_PL_Y = 0.05196152422f; // Break placement Y const float k_BALL_1OR = 33.3333333333f; // 1 over ball radius const float k_BALL_RSQR = 0.0009f; // ball radius squared const float k_BALL_DSQR = 0.0036f; // ball diameter squared const float k_BALL_DSQRPE = 0.003598f; // ball diameter squared plus epsilon const float k_CUSHION_RSTT = 0.79f; // Coefficient of restituion against cushion const float k_PI_2 = 1.57079632679f; // Pi over 2 const float k_BALL_RADIUS = 0.03f; float k_MINOR_REGION_CONST; const float k_1OR2 = 0.70710678118f; // 1 over root 2 (normalize +-1,+-1 vector) const float k_1OR5 = 0.4472135955f; // 1 over root 5 (normalize +-1,+-2 vector) const float k_RANDOMIZE_F = 0.0001f; const float k_POCKET_DEPTH = 0.04f; // How far back (roughly) do pockets absorb balls after this point const float k_MIN_VELOCITY = 0.00005625f; // SQUARED const float k_F_SLIDE = 0.2f; // Friction coefficient of sliding const float k_F_ROLL = 0.01f; // Friction coefficient of rolling const float k_SPOT_POSITION_X = 0.5334f; // First X position of the racked balls const float k_SPOT_CAROM_X = 0.8001f; // Spot position for carom mode const float k_RACK_HEIGHT = -0.0764f; // Rack position on Y axis const float k_GRAVITY = 9.80665f; // Earths gravitational acceleration const float k_BALL_MASS = 0.16f; // Weight of ball in kg Vector3 k_CONTACT_POINT = new Vector3(0.0f, -0.03f, 0.0f); #if HT_QUEST uint ANDROID_UNIFORM_CLOCK = 0x00u; uint ANDROID_CLOCK_DIVIDER = 0x8u; #endif // Colour constants Color k_markerColourOK = new Color(0.0f, 1.0f, 0.0f, 1.0f); Color k_markerColourNO = new Color(1.0f, 0.0f, 0.0f, 1.0f); Color k_gripColourActive = new Color(0.0f, 0.5f, 1.1f, 1.0f); Color k_gripColourInactive = new Color(0.34f, 0.34f, 0.34f, 1.0f); Color k_aimColour_aim = new Color(0.7f, 0.7f, 0.7f, 1.0f); Color k_aimColour_locked = new Color(1.0f, 1.0f, 1.0f, 1.0f); const string LOG_LOW = ""; const string LOG_ERR = ""; const string LOG_WARN = ""; const string LOG_YES = ""; const string LOG_END = ""; #endregion #region R_INSPECTOR [Space(10)] [Header("Materials")] [SerializeField] Material guidelineMat; [SerializeField] Material[] CueGripMaterials; [SerializeField] Material markerMaterial; [SerializeField] Material timerMaterial; // Textures [Header("Textures")] [SerializeField] Texture scorecard8ball; [SerializeField] Texture scorecard4ball; [SerializeField] Texture scorecard9ball; [Space(10)] [Header("Sound Effects")] [SerializeField] AudioClip snd_Intro; [SerializeField] AudioClip snd_Sink; [SerializeField] AudioClip[] snd_Hits; [SerializeField] AudioClip snd_NewTurn; [SerializeField] AudioClip snd_PointMade; //[SerializeField] AudioClip snd_bad; [SerializeField] AudioClip snd_btn; [SerializeField] AudioClip snd_spin; [SerializeField] AudioClip snd_spinstop; [SerializeField] AudioClip snd_hitball; [Space(10)] [Header("Internal (no touching!)")] // Other scripts [SerializeField] ht8b_cue[] gripControllers; // GameObjects [SerializeField] public GameObject[] balls_render; [SerializeField] public GameObject cuetip; [SerializeField] GameObject guideline; [SerializeField] GameObject guidefspin; [SerializeField] GameObject devhit; [SerializeField] GameObject[] playerTotems; [SerializeField] GameObject[] cueTips; [SerializeField] GameObject infBaseTransform; [SerializeField] GameObject markerObj; [SerializeField] GameObject marker9ball; [SerializeField] GameObject tableoverlayUI; [SerializeField] GameObject m_base; [SerializeField] GameObject point4ball; [SerializeField] GameObject[] cueRenderObjs; [SerializeField] GameObject select4b; [SerializeField] GameObject[] timers; // 1.8 + (config autos) GameObject auto_rackPosition; GameObject auto_pocketblockers; GameObject auto_colliderBaseVFX; // Meshes //[SerializeField] Mesh[] cueball_meshes; //[SerializeField] Mesh _9ball_mesh; [SerializeField] Mesh _4ball_mesh_add; [SerializeField] Mesh _4ball_mesh_minus; // Texts [SerializeField] Text ltext; [SerializeField] Text[] playerNames; [SerializeField] Text infText; [SerializeField] Text infReset; // Renderers [SerializeField] Material scoreCardMat; [SerializeField] ReflectionProbe reflection_main; [SerializeField] VRC.Udon.UdonBehaviour bhv_cfgData; #endregion // Audio Components AudioSource aud_main; #region R_GAMESTATE [UdonSynced] private string netstr; // dumpster fire private string netstr_prv; byte[] net_data = new byte[0x52]; // Networked gamestate // data positions are marked as <#ushort>:<#bit> () uint sn_pocketed = 0x00U; // 18:0 (0xffff) Each bit represents each ball, if it has been pocketed or not bool sn_simulating = false; // 19:0 (0x01) True whilst balls are rolling uint sn_turnid = 0x00U; // 19:1 (0x02) Whos turn is it, 0 or 1 bool sn_foul = false; // 19:2 (0x04) End-of-turn foul marker bool sn_open = true; // 19:3 (0x08) Is the table open? uint sn_playerxor = 0x00; // 19:4 (0x10) What colour the players have chosen bool sn_gameover = true; // 19:5 (0x20) Game is complete uint sn_winnerid = 0x00U; // 19:6 (0x40) Who won the game if sn_gameover is set bool sn_lobbyclosed = true; [HideInInspector] public bool sn_permit = false; // 19:7 (0x80) Permission for player to play uint sn_gamemode = 0; // 19:8 (0x700) Gamemode ID 3 bit { 0: 8 ball, 1: 9 ball, 2+: undefined } uint sn_timer = 0; // 19:13 (0x6000) Timer ID 2 bit { 0: inf, 1: 60s, 2: 30s, 3: undefined } bool sn_teams = false; // 19:15 (0x8000) Teams on/off (1 bit) ushort sn_packetid = 0; // 20:0 (0xffff) Current packet number, used for locking updates so we dont accidently go back. // this behaviour was observed on some long connections so its necessary ushort sn_gameid = 0; // 21:0 (0xffff) Game number ushort sn_gmspec = 0; // 22:0 (0xffff) Game mode specific information // Cannot making a struct in C#, therefore values are duplicated // for gamestate deltas uint sn_pocketed_prv; uint sn_turnid_prv; bool sn_open_prv; bool sn_gameover_prv; ushort sn_gameid_prv; uint sn_gamemode_prv; uint sn_timer_prv; bool sn_teams_prv; bool sn_lobbyclosed_prv; // Local gamestates [HideInInspector] public bool sn_armed = false; // Player is hitting bool sn_updatelock = false; // We are waiting for our local simulation to finish, before we unpack data int sn_firsthit = 0; // The first ball to be hit by cue ball int sn_secondhit = 0; int sn_thirdhit = 0; bool sn_oursim = false; // If the simulation was initiated by us, only set from update byte sn_wins0 = 0; // Wins for player 0 (unused) byte sn_wins1 = 0; // Wins for player 1 (unused) float introAminTimer = 0.0f; // Ball dropper timer bool ballsMoving = false; // Tracker variable to see if balls are still on the go bool isReposition = false; // Repositioner is active float repoMaxX; // For clamping to table or set lower for kitchen float timer_end = 0.0f; // What should the timer run out at float timer_recip = 0.0f; // 1 over time limit bool timer_running = false; bool particle_alive = false; float particle_time = 0.0f; bool fb_madepoint = false; bool fb_madefoul = false; bool fb_jp = false; bool fb_kr = false; int[] fb_scores = new int[2]; bool gm_is_0 = false; bool gm_is_1 = false; bool gm_is_2 = false; //bool gm_is_3 = false; bool gm_practice = false; // Game should run in practice mode bool region_selected = false; bool dk_shootui = false; // Overide / standard mesh packs Mesh[] meshes_balls_override = new Mesh[4]; // Currently used for 4 ball mode Mesh[] meshes_balls_regular = new Mesh[4]; // Values that will get sucked in from the menu [HideInInspector] public int local_playerid = -1; uint local_teamid = 0u; // Interpreted value VRCPlayerApi[] start_saved_players = new VRCPlayerApi[2]; #endregion #region R_PHYS_MEM Vector3[] ball_CO = new Vector3[16]; // Current positions Vector3[] ball_V = new Vector3[16]; // Current velocities Vector3[] ball_W = new Vector3[16]; // Angular velocities #endregion // Shader uniforms // *udon currently does not support integer uniform identifiers #if USE_INT_UNIFORMS int uniform_tablecolour; int uniform_scorecard_colour0; int uniform_scorecard_colour1; int uniform_scorecard_info; int uniform_marker_colour; int uniform_cue_colour; #else const string uniform_tablecolour = "_EmissionColor"; const string uniform_clothcolour = "_Color"; const string uniform_scorecard_colour0 = "_Colour0"; const string uniform_scorecard_colour1 = "_Colour1"; const string uniform_scorecard_info = "_Info"; const string uniform_marker_colour = "_Color"; const string uniform_cue_colour = "_ReColor"; public name_gen _name_gen; #endif #region R_VFX Color tableSrcColour = new Color(1.0f, 1.0f, 1.0f, 1.0f); // Runtime target colour Color tableCurrentColour = new Color(1.0f, 1.0f, 1.0f, 1.0f); // Runtime actual colour // 'Pointer' colours. Color pColour0; // Team 0 Color pColour1; // Team 1 Color pColour2; // No team / open / 9 ball Color pColourErr; Color pClothColour; // Updates table colour target to appropriate player colour void _vis_apply_tablecolour(uint idsrc) { if (gm_is_2) { if (sn_turnid == 0) { cueRenderObjs[0].GetComponent().sharedMaterial.SetColor(uniform_cue_colour, pColour0); cueRenderObjs[1].GetComponent().sharedMaterial.SetColor(uniform_cue_colour, pColour1 * 0.333f); } else { cueRenderObjs[0].GetComponent().sharedMaterial.SetColor(uniform_cue_colour, pColour0 * 0.333f); cueRenderObjs[1].GetComponent().sharedMaterial.SetColor(uniform_cue_colour, pColour1); } if ((idsrc ^ sn_playerxor) == 0) { // Set table colour to blue tableSrcColour = pColour0; } else { // Table colour to orange tableSrcColour = pColour1; } } else if (gm_is_1) { cueRenderObjs[sn_turnid].GetComponent().sharedMaterial.SetColor(uniform_cue_colour, k_colour_default); cueRenderObjs[sn_turnid ^ 0x1u].GetComponent().sharedMaterial.SetColor(uniform_cue_colour, k_colour_off); tableSrcColour = pColour2; } else { if (!sn_open) { if ((idsrc ^ sn_playerxor) == 0) { // Set table colour to blue tableSrcColour = pColour0; } else { // Table colour to orange tableSrcColour = pColour1; } cueRenderObjs[sn_playerxor].GetComponent().sharedMaterial.SetColor(uniform_cue_colour, pColour0); cueRenderObjs[sn_playerxor ^ 0x1u].GetComponent().sharedMaterial.SetColor(uniform_cue_colour, pColour1); } else { tableSrcColour = pColour2; cueRenderObjs[sn_turnid].GetComponent().sharedMaterial.SetColor(uniform_cue_colour, k_colour_default); cueRenderObjs[sn_turnid ^ 0x1u].GetComponent().sharedMaterial.SetColor(uniform_cue_colour, k_colour_off); } } CueGripMaterials[sn_turnid].SetColor(uniform_marker_colour, k_gripColourActive); CueGripMaterials[sn_turnid ^ 0x1u].SetColor(uniform_marker_colour, k_gripColourInactive); } void _vis_hidetimers() { timers[0].SetActive(false); timers[1].SetActive(false); } void _vis_showballs() { if (gm_is_1) { for (int i = 0; i <= 9; i++) balls_render[i].SetActive(true); for (int i = 10; i < 16; i++) balls_render[i].SetActive(false); } else if (gm_is_2) { for (int i = 1; i < 16; i++) balls_render[i].SetActive(false); balls_render[0].SetActive(true); balls_render[13].SetActive(true); balls_render[14].SetActive(true); balls_render[15].SetActive(true); } else { for (int i = 0; i < 16; i++) { balls_render[i].SetActive(true); } } } public void _vis_updatecoloursources() { if (gm_is_1) // 9 Ball / USA colours { pColour0 = k_colour_default; pColour1 = k_colour_default; pColour2 = k_colour_default; pColourErr = k_colour_default; // No error effect pClothColour = k_fabricColour_9ball; // 9 ball only uses one colourset / cloth colour ballMaterial.SetTexture("_MainTex", textureSets[1]); } else if (gm_is_2) { pColour0 = k_colour4Ball_team_0; pColour1 = k_colour4Ball_team_1; // Should not be used pColour2 = k_colour_foul; pColourErr = k_colour_foul; ballMaterial.SetTexture("_MainTex", textureSets[1]); pClothColour = k_fabricColour_4ball; } else // Standard 8 ball derivatives { pColourErr = k_colour_foul; pColour2 = k_colour_default; pColour0 = k_teamColour_spots; pColour1 = k_teamColour_stripes; ballMaterial.SetTexture("_MainTex", textureSets[0]); pClothColour = k_fabricColour_8ball; } tableMaterial.SetColor(uniform_clothcolour, pClothColour); } void _vis_disableobjects() { guideline.SetActive(false); devhit.SetActive(false); infBaseTransform.SetActive(false); markerObj.SetActive(false); tableoverlayUI.SetActive(false); marker9ball.SetActive(false); point4ball.SetActive(false); select4b.SetActive(false); _vis_hidetimers(); } void _vis_spawn_floaty(Vector3 pos, Mesh m) { point4ball.SetActive(true); particle_alive = true; particle_time = 0.1f; // orient to be looking at player Vector3 lpos = Networking.LocalPlayer.GetPosition(); Vector3 delta = lpos - transform_Surface.TransformPoint(pos); float r = Mathf.Atan2(delta.x, delta.z); point4ball.transform.localRotation = Quaternion.AngleAxis(r * Mathf.Rad2Deg, Vector3.up); // set position point4ball.transform.localPosition = pos; // Set scale point4ball.transform.localScale = Vector3.zero; point4ball.GetComponent().sharedMesh = m; } void _vis_floaty_eval() { float scale, s, v, e; // Evaluate time particle_time += Time.deltaTime * 0.25f; // Sustained step s = Mathf.Max(particle_time - 0.1f, 0.0f); v = Mathf.Min(particle_time * particle_time * 100.0f, 21.0f * s * Mathf.Exp(-15.0f * s)); // Exponential step e = Mathf.Exp(-17.0f * Mathf.Pow(Mathf.Max(particle_time - 1.2f, 0.0f), 3.0f)); scale = e * v * 2.0f; // Set scale point4ball.transform.localScale = new Vector3(scale, scale, scale); // Set position Vector3 temp = point4ball.transform.localPosition; temp.y = particle_time * 0.5f; point4ball.transform.localPosition = temp; // Particle death if (particle_time > 2.0f) { particle_alive = false; point4ball.SetActive(false); } } #endregion void _timer_reset() { if (sn_timer == 2) // 30s { timer_end = Time.timeSinceLevelLoad + 30.0f; timer_recip = 0.03333333333f; } else // 60s { timer_end = Time.timeSinceLevelLoad + 60.0f; timer_recip = 0.01666666666f; } timers[0].SetActive(true); timers[1].SetActive(true); timer_running = true; } #region R_LOCALEV void _onlocal_carompoint(Vector3 p) { fb_madepoint = true; aud_main.PlayOneShot(snd_PointMade, 1.0f); fb_scores[sn_turnid]++; if (fb_scores[sn_turnid] > 10) { fb_scores[sn_turnid] = 10; } _vis_spawn_floaty(p, _4ball_mesh_add); } void _onlocal_carompenalize(Vector3 p) { fb_madefoul = true; //aud_main.PlayOneShot( snd_bad, 1.0f ); fb_scores[sn_turnid]--; if (fb_scores[sn_turnid] < 0) { fb_scores[sn_turnid] = 0; } _vis_spawn_floaty(p, _4ball_mesh_minus); } // Called when a player first sinks a ball whilst the table was previously open void _onlocal_tableclosed() { uint picker = sn_turnid ^ sn_playerxor; #if HT8B_DEBUGGER _log( LOG_YES + "(local) " + Networking.GetOwner( playerTotems[ sn_turnid ] ).displayName + ":" + sn_turnid + " is " + (picker == 0? "blues": "oranges") + LOG_END ); #endif _vis_apply_tablecolour(sn_turnid); _onlocal_updatescorecard(); scoreCardMat.SetColor(uniform_scorecard_colour0, sn_playerxor == 0 ? pColour0 : pColour1); scoreCardMat.SetColor(uniform_scorecard_colour1, sn_playerxor == 1 ? pColour0 : pColour1); } // End of the game. Both with/loss void _onlocal_gameover() { _vis_apply_tablecolour(sn_winnerid); #if HT8B_DEBUGGER _log( LOG_WARN + sn_packetid + " >> local: " + local_playerid + " Winner: " + sn_winnerid + LOG_END ); #endif if (start_saved_players[sn_winnerid] != null) { #if HT8B_DEBUGGER _log( LOG_YES + "(local) Winner of match: " + start_saved_players[ sn_winnerid ].displayName + LOG_END ); #endif infText.text = start_saved_players[sn_winnerid].displayName + " wins!"; } else { infText.text = "[Unknown player] wins!"; } infBaseTransform.SetActive(true); marker9ball.SetActive(false); tableoverlayUI.SetActive(false); #if !HT_QUEST _vis_rackballs(); // To make sure rigidbodies are completely off #endif _vis_hidetimers(); isReposition = false; markerObj.SetActive(false); _onlocal_updatescorecard(); // Remove any access rights local_playerid = -1; _apply_cue_access(); _htmenu_enter(); } void _onlocal_turnchange() { // Effects _vis_apply_tablecolour(sn_turnid); aud_main.PlayOneShot(snd_NewTurn, 1.0f); // Register correct cuetip cuetip = cueTips[sn_turnid]; bool isOurTurn = ((local_playerid >= 0) && (local_teamid == sn_turnid)) || gm_practice; if (gm_is_2) // 4 ball { // Swap cue ball and opponent cue ball Vector3 temp = ball_CO[0]; ball_CO[0] = ball_CO[13]; ball_CO[13] = temp; // Swap visual meshes if (sn_turnid == 0) { balls_render[0].GetComponent().sharedMesh = meshes_balls_override[0]; balls_render[13].GetComponent().sharedMesh = meshes_balls_override[1]; } else { balls_render[13].GetComponent().sharedMesh = meshes_balls_override[0]; balls_render[0].GetComponent().sharedMesh = meshes_balls_override[1]; } } else { // White was pocketed if ((sn_pocketed & 0x1u) == 0x1u) { ball_CO[0] = Vector3.zero; ball_V[0] = Vector3.zero; ball_W[0] = Vector3.zero; sn_pocketed &= 0xFFFFFFFEu; } } if (isOurTurn) { if (sn_foul) { isReposition = true; repoMaxX = k_pR.x; markerObj.SetActive(true); markerObj.transform.localPosition = ball_CO[0]; } } // Force timer reset if (sn_timer > 0) { _timer_reset(); } } void _onlocal_updatescorecard() { if (gm_is_2) { scoreCardMat.SetVector(uniform_scorecard_info, new Vector4(fb_scores[0] * 0.04681905f, fb_scores[1] * 0.04681905f, 0.0f, 0.0f)); } else { int[] counter0 = new int[2]; uint temp = sn_pocketed; for (int j = 0; j < 2; j++) { for (int i = 0; i < 7; i++) { if ((temp & 0x4) > 0) { counter0[j ^ sn_playerxor]++; } temp >>= 1; } } // Add black ball if we are winning the thing if (sn_gameover) { counter0[sn_winnerid] += (int)((sn_pocketed & 0x2) >> 1); } scoreCardMat.SetVector(uniform_scorecard_info, new Vector4(counter0[0] * 0.0625f, counter0[1] * 0.0625f, 0.0f, 0.0f)); } } // Player scored an objective ball void _onlocal_pocket_obj() { // Make a bright flash tableCurrentColour *= 1.9f; aud_main.PlayOneShot(snd_Sink, 1.0f); } // Player scored a foul ball (cue, non-objective or 8 before set cleared) void _onlocal_pocket_enm() { tableCurrentColour = pColourErr; aud_main.PlayOneShot(snd_Sink, 1.0f); } // once balls stops rolling this is called void _onlocal_sim_end() { sn_simulating = false; #if HT8B_DEBUGGER _log( LOG_LOW + "(local) SimEnd()" + LOG_END ); #endif // Make sure we only run this from the client who initiated the move if (sn_oursim) { sn_oursim = false; // We are updating the game state so make sure we are network owner Networking.SetOwner(Networking.LocalPlayer, this.gameObject); // Owner state checks #if HT8B_DEBUGGER _log( LOG_LOW + "Post-move state checking" + LOG_END ); #endif uint bmask = 0xFFFCu; uint emask = 0x0u; // Quash down the mask if table has closed if (!sn_open) { bmask = bmask & (0x1FCu << ((int)(sn_playerxor ^ sn_turnid) * 7)); emask = 0x1FCu << ((int)(sn_playerxor ^ sn_turnid ^ 0x1U) * 7); } // Common informations bool isSetComplete = (sn_pocketed & bmask) == bmask; bool isScratch = (sn_pocketed & 0x1U) == 0x1U; // Append black to mask if set is done if (isSetComplete) { bmask |= 0x2U; } // These are the resultant states we can set for each mode // then the rest is taken care of bool isObjectiveSink, isOpponentSink, winCondition, foulCondition, deferLossCondition ; if (gm_is_0) // Standard 8 ball { isObjectiveSink = (sn_pocketed & bmask) > (sn_pocketed_prv & bmask); isOpponentSink = (sn_pocketed & emask) > (sn_pocketed_prv & emask); // Calculate if objective was not hit first bool isWrongHit = ((0x1U << sn_firsthit) & bmask) == 0; bool is8Sink = (sn_pocketed & 0x2U) == 0x2U; winCondition = isSetComplete && is8Sink; foulCondition = isScratch || isWrongHit; deferLossCondition = is8Sink; } else if (gm_is_1) // 9 ball { // Rules are from: https://www.youtube.com/watch?v=U0SbHOXCtFw // Rule #1: Cueball must strike the lowest number ball, first bool isWrongHit = !(_lowest_ball(sn_pocketed_prv) == sn_firsthit); // Rule #2: Pocketing cueball, is a foul // Win condition: Pocket 9 ball ( at anytime ) winCondition = (sn_pocketed & 0x200u) == 0x200u; // this video is hard to follow so im just gonna guess this is right isObjectiveSink = (sn_pocketed & 0x3FEu) > (sn_pocketed_prv & 0x3FEu); isOpponentSink = false; deferLossCondition = false; foulCondition = isWrongHit || isScratch; // TODO: Implement rail contact requirement } else if (gm_is_2) // 4 ball { isObjectiveSink = fb_madepoint; isOpponentSink = fb_madefoul; foulCondition = false; deferLossCondition = false; winCondition = fb_scores[sn_turnid] >= 10; } else // Unkown mode { isObjectiveSink = true; isOpponentSink = false; winCondition = false; foulCondition = false; deferLossCondition = false; if ((sn_pocketed & 0x1u) == 0x1u) { sn_foul = true; _onlocal_turnchange(); } } if (winCondition) { if (foulCondition) { // Loss _turn_win(sn_turnid ^ 0x1U); } else { // Win _turn_win(sn_turnid); } } else if (deferLossCondition) { // Loss _turn_win(sn_turnid ^ 0x1U); } else if (foulCondition) { // Foul _turn_foul(); } else if (isObjectiveSink && !isOpponentSink) { // Continue _turn_continue(); } else { // Pass _turn_pass(); } } // Check if there was a network update on hold if (sn_updatelock) { #if HT8B_DEBUGGER _log( LOG_LOW + "Update was waiting, executing now" + LOG_END ); #endif sn_updatelock = false; _netread(); } } void _onlocal_timer_end() { timer_running = false; #if HT8B_DEBUGGER _log( LOG_ERR + "Out of time!!" + LOG_END ); #endif // We are holding the stick so propogate the change if (Networking.GetOwner(playerTotems[sn_turnid]) == Networking.LocalPlayer) { Networking.SetOwner(Networking.LocalPlayer, this.gameObject); _turn_foul(); } else { // All local players freeze until next target // can pick up and propogate timer end sn_permit = false; } _vis_hidetimers(); } // Grant cue access if we are playing void _apply_cue_access() { if (local_playerid >= 0) { if (gm_practice) { gripControllers[0]._access_allow(); gripControllers[1]._access_allow(); } else { if ((local_teamid & 0x1) > 0) // Local player is 1, or 3 { gripControllers[1]._access_allow(); gripControllers[0]._access_deny(); } else // Local player is 0, or 2 { gripControllers[0]._access_allow(); gripControllers[1]._access_deny(); } } } else { gripControllers[0]._access_deny(); gripControllers[1]._access_deny(); } } // Some udon specific optimisations void _setup_gm_spec() { gm_is_0 = sn_gamemode == 0u; gm_is_1 = sn_gamemode == 1u; gm_is_2 = sn_gamemode == 2u; } void _onlocal_newgame() { #if HT8B_DEBUGGER _log( LOG_LOW + "NewGameLocal()" + LOG_END ); #endif // Take a copy of player apis start_saved_players[0] = Networking.GetOwner(playerTotems[0]); start_saved_players[1] = Networking.GetOwner(playerTotems[1]); _setup_gm_spec(); // Calculate interpreted values from menu states if (local_playerid >= 0) local_teamid = (uint)local_playerid & 0x1u; // Disable menu _htmenu_exit(); // Reflect menu-state settings (for late joiners) _vis_updatecoloursources(); _vis_apply_tablecolour(0); _apply_cue_access(); if (gm_is_2) // 4 ball specific { auto_pocketblockers.SetActive(true); scoreCardMat.SetTexture("_MainTex", scorecard4ball); scoreCardMat.SetColor(uniform_scorecard_colour0, pColour0); scoreCardMat.SetColor(uniform_scorecard_colour1, pColour1); fb_scores[0] = 0; fb_scores[1] = 0; // Set mesh filters on balls that change them balls_render[0].GetComponent().sharedMesh = meshes_balls_override[0]; balls_render[13].GetComponent().sharedMesh = meshes_balls_override[1]; balls_render[14].GetComponent().sharedMesh = meshes_balls_override[2]; balls_render[15].GetComponent().sharedMesh = meshes_balls_override[3]; } else { auto_pocketblockers.SetActive(false); scoreCardMat.SetTexture("_MainTex", scorecard8ball); // Reset mesh filters on balls that change them balls_render[0].GetComponent().sharedMesh = meshes_balls_regular[0]; balls_render[13].GetComponent().sharedMesh = meshes_balls_regular[1]; balls_render[14].GetComponent().sharedMesh = meshes_balls_regular[2]; balls_render[15].GetComponent().sharedMesh = meshes_balls_regular[3]; } if (gm_is_1) // 9 ball specific { scoreCardMat.SetTexture("_MainTex", scorecard9ball); marker9ball.SetActive(true); } else { marker9ball.SetActive(false); } _vis_showballs(); // Reflect game state _onlocal_updatescorecard(); isReposition = false; markerObj.SetActive(false); infBaseTransform.SetActive(false); // Effects introAminTimer = 2.0f; aud_main.PlayOneShot(snd_Intro, 1.0f); // Player name texts string base_text = ""; if (sn_teams) { base_text = "Team "; } tableoverlayUI.SetActive(true); playerNames[0].text = base_text + Networking.GetOwner(playerTotems[0]).displayName; playerNames[1].text = base_text + Networking.GetOwner(playerTotems[1]).displayName; timer_running = false; // Switch desktop/vr bool usr_desktop = !Networking.LocalPlayer.IsUserInVR(); #if !HT_QUEST gripControllers[0].useDesktop = usr_desktop; gripControllers[1].useDesktop = usr_desktop; #endif reflection_main.RenderProbe(); } #endregion #region R_PHYS // Cue input tracking Vector3 cue_lpos; Vector3 cue_llpos; Vector3 cue_vdir; Vector3 cue_shotdir; float cue_fdir; #if HT_QUEST #else [HideInInspector] public Vector3 dkTargetPos; // Target for desktop aiming #endif // Finalize positions onto their rack spots void _vis_rackballs() { uint ball_bit = 0x1u; for (int i = 0; i < 16; i++) { balls_render[i].GetComponent().isKinematic = true; if ((ball_bit & sn_pocketed) == ball_bit) { // Recover Y position since its lost in networking Vector3 rack_position = ball_CO[i]; rack_position.y = k_rack_position.y; balls_render[i].transform.localPosition = rack_position; } ball_bit <<= 1; } } // Internal game state pocket and enable unity physics to play out the rest void _onlocal_pocketball(int id) { uint total = 0U; // Get total for X positioning int count_extent = gm_is_1 ? 10 : 16; for (int i = 1; i < count_extent; i++) { total += (sn_pocketed >> i) & 0x1U; } // place ball on the rack ball_CO[id] = k_rack_position + (float)total * k_BALL_DIAMETRE * k_rack_direction; sn_pocketed ^= 1U << id; uint bmask = 0x1FCU << ((int)(sn_turnid ^ sn_playerxor) * 7); // Good pocket if (((0x1U << id) & ((bmask) | (sn_open ? 0xFFFCU : 0x0000U) | ((bmask & sn_pocketed) == bmask ? 0x2U : 0x0U))) > 0) { _onlocal_pocket_obj(); } else { // bad _onlocal_pocket_enm(); } #if !HT_QUEST // VFX ( make ball move ) Rigidbody body = balls_render[id].GetComponent(); body.isKinematic = false; body.velocity = this.transform.TransformVector(new Vector3( ball_V[id].x, 0.0f, ball_V[id].z )); #else balls_render[ id ].transform.localPosition = ball_CO[ id ]; #endif } // Is cue touching another ball? bool _cue_contacting() { if (gm_is_0) // 8 ball { // Check all for (int i = 1; i < 16; i++) { if ((ball_CO[0] - ball_CO[i]).sqrMagnitude < k_BALL_DSQR) { return true; } } } else if (gm_is_1) // 9 { // Only check to 9 ball for (int i = 1; i <= 9; i++) { if ((ball_CO[0] - ball_CO[i]).sqrMagnitude < k_BALL_DSQR) { return true; } } } else // 4 { if ((ball_CO[0] - ball_CO[9]).sqrMagnitude < k_BALL_DSQR) { return true; } if ((ball_CO[0] - ball_CO[2]).sqrMagnitude < k_BALL_DSQR) { return true; } if ((ball_CO[0] - ball_CO[3]).sqrMagnitude < k_BALL_DSQR) { return true; } } return false; } const float k_SINA = 0.28078832987f; const float k_SINA2 = 0.07884208619f; const float k_COSA = 0.95976971915f; const float k_COSA2 = 0.92115791379f; const float k_EP1 = 1.79f; const float k_A = 21.875f; const float k_B = 6.25f; const float k_F = 1.72909790282f; // Apply cushion bounce void _phy_bounce_cushion(int id, Vector3 N) { // Mathematical expressions derived from: https://billiards.colostate.edu/physics_articles/Mathavan_IMechE_2010.pdf // // (Note): subscript gamma, u, are used in replacement of Y and Z in these expressions because // unicode does not have them. // // f = 2/7 // f₁ = 5/7 // // Velocity delta: // Δvₓ = −vₓ∙( f∙sin²θ + (1+e)∙cos²θ ) − Rωᵤ∙sinθ // Δvᵧ = 0 // Δvᵤ = f₁∙vᵤ + fR( ωₓ∙sinθ - ωᵧ∙cosθ ) - vᵤ // // Aux: // Sₓ = vₓ∙sinθ - vᵧ∙cosθ+ωᵤ // Sᵧ = 0 // Sᵤ = -vᵤ - ωᵧ∙cosθ + ωₓ∙cosθ // // k = (5∙Sᵤ) / ( 2∙mRA ) // c = vₓ∙cosθ - vᵧ∙cosθ // // Angular delta: // ωₓ = k∙sinθ // ωᵧ = k∙cosθ // ωᵤ = (5/(2m))∙(-Sₓ / A + ((sinθ∙c∙(e+1)) / B)∙(cosθ - sinθ)) // // These expressions are in the reference frame of the cushion, so V and ω inputs need to be rotated // Reject bounce if velocity is going the same way as normal // this state means we tunneled, but it happens only on the corner // vertexes Vector3 source_v = ball_V[id]; if (Vector3.Dot(source_v, N) > 0.0f) { return; } // Rotate V, W to be in the reference frame of cushion Quaternion rq = Quaternion.AngleAxis(Mathf.Atan2(-N.z, -N.x) * Mathf.Rad2Deg, Vector3.up); Quaternion rb = Quaternion.Inverse(rq); Vector3 V = rq * source_v; Vector3 W = rq * ball_W[id]; Vector3 V1; Vector3 W1; float k, c, s_x, s_z; //V1.x = -V.x * ((2.0f/7.0f) * k_SINA2 + k_EP1 * k_COSA2) - (2.0f/7.0f) * k_BALL_PL_X * W.z * k_SINA; //V1.z = (5.0f/7.0f)*V.z + (2.0f/7.0f) * k_BALL_PL_X * (W.x * k_SINA - W.y * k_COSA) - V.z; //V1.y = 0.0f; // (baked): V1.x = -V.x * k_F - 0.00240675711f * W.z; V1.z = 0.71428571428f * V.z + 0.00857142857f * (W.x * k_SINA - W.y * k_COSA) - V.z; V1.y = 0.0f; // s_x = V.x * k_SINA - V.y * k_COSA + W.z; // (baked): y component not used: s_x = V.x * k_SINA + W.z; s_z = -V.z - W.y * k_COSA + W.x * k_SINA; // k = (5.0f * s_z) / ( 2 * k_BALL_MASS * k_A ); // (baked): k = s_z * 0.71428571428f; // c = V.x * k_COSA - V.y * k_COSA; // (baked): y component not used c = V.x * k_COSA; W1.x = k * k_SINA; //W1.z = (5.0f / (2.0f * k_BALL_MASS)) * (-s_x / k_A + ((k_SINA * c * k_EP1) / k_B) * (k_COSA - k_SINA)); // (baked): W1.z = 15.625f * (-s_x * 0.04571428571f + c * 0.0546021744f); W1.y = k * k_COSA; // Unrotate result ball_V[id] += rb * V1; ball_W[id] += rb * W1; } Vector3 k_vA = new Vector3(); Vector3 k_vB = new Vector3(); Vector3 k_vC = new Vector3(); Vector3 k_vD = new Vector3(); Vector3 k_vX = new Vector3(); Vector3 k_vY = new Vector3(); Vector3 k_vZ = new Vector3(); Vector3 k_vW = new Vector3(); Vector3 k_pK = new Vector3(); Vector3 k_pL = new Vector3(); Vector3 k_pM = new Vector3(); Vector3 k_pN = new Vector3(); Vector3 k_pO = new Vector3(); Vector3 k_pP = new Vector3(); Vector3 k_pQ = new Vector3(); Vector3 k_pR = new Vector3(); Vector3 k_pT = new Vector3(); Vector3 k_pS = new Vector3(); Vector3 k_pU = new Vector3(); Vector3 k_pV = new Vector3(); Vector3 k_vA_vD = new Vector3(); Vector3 k_vA_vD_normal = new Vector3(); Vector3 k_vB_vY = new Vector3(); Vector3 k_vB_vY_normal = new Vector3(); Vector3 k_vC_vZ_normal = new Vector3(); Vector3 k_vA_vB_normal = new Vector3(0.0f, 0.0f, -1.0f); Vector3 k_vC_vW_normal = new Vector3(-1.0f, 0.0f, 0.0f); Vector3 _sign_pos = new Vector3(0.0f, 1.0f, 0.0f); Vector3 k_rack_position = new Vector3(); Vector3 k_rack_direction = new Vector3(); Transform transform_Surface; void _phy_table_init() { k_rack_position = transform_Surface.InverseTransformPoint(auto_rackPosition.transform.position); k_rack_direction = transform_Surface.InverseTransformDirection(auto_rackPosition.transform.up); // Handy values k_MINOR_REGION_CONST = k_TABLE_WIDTH - k_TABLE_HEIGHT; // Major source vertices k_vA.x = k_POCKET_RADIUS * 0.92f; k_vA.z = k_TABLE_HEIGHT; k_vB.x = k_TABLE_WIDTH - k_POCKET_RADIUS; k_vB.z = k_TABLE_HEIGHT; k_vC.x = k_TABLE_WIDTH; k_vC.z = k_TABLE_HEIGHT - k_POCKET_RADIUS; k_vD.x = k_vA.x - 0.016f; k_vD.z = k_vA.z + 0.060f; // Aux points k_vX = k_vD + Vector3.forward; k_vW = k_vC; k_vW.z = 0.0f; k_vY = k_vB; k_vY.x += 1.0f; k_vY.z += 1.0f; k_vZ = k_vC; k_vZ.x += 1.0f; k_vZ.z += 1.0f; // Normals k_vA_vD = k_vD - k_vA; k_vA_vD = k_vA_vD.normalized; k_vA_vD_normal.x = -k_vA_vD.z; k_vA_vD_normal.z = k_vA_vD.x; k_vB_vY = k_vB - k_vY; k_vB_vY = k_vB_vY.normalized; k_vB_vY_normal.x = -k_vB_vY.z; k_vB_vY_normal.z = k_vB_vY.x; k_vC_vZ_normal = -k_vB_vY_normal; // Minkowski difference k_pN = k_vA; k_pN.z -= k_CUSHION_RADIUS; k_pM = k_vA + k_vA_vD_normal * k_CUSHION_RADIUS; k_pL = k_vD + k_vA_vD_normal * k_CUSHION_RADIUS; k_pK = k_vD; k_pK.x -= k_CUSHION_RADIUS; k_pO = k_vB; k_pO.z -= k_CUSHION_RADIUS; k_pP = k_vB + k_vB_vY_normal * k_CUSHION_RADIUS; k_pQ = k_vC + k_vC_vZ_normal * k_CUSHION_RADIUS; k_pR = k_vC; k_pR.x -= k_CUSHION_RADIUS; k_pT = k_vX; k_pT.x -= k_CUSHION_RADIUS; k_pS = k_vW; k_pS.x -= k_CUSHION_RADIUS; k_pU = k_vY + k_vB_vY_normal * k_CUSHION_RADIUS; k_pV = k_vZ + k_vC_vZ_normal * k_CUSHION_RADIUS; k_pS = k_vW; k_pS.x -= k_CUSHION_RADIUS; } // Check pocket condition void _phy_ball_pockets(int id) { Vector3 A; A = ball_CO[id]; _sign_pos.x = Mathf.Sign(A.x); _sign_pos.z = Mathf.Sign(A.z); A = Vector3.Scale(A, _sign_pos); if (Vector3.Distance(A, k_vE) < k_INNER_RADIUS) { _onlocal_pocketball(id); return; } if (Vector3.Distance(A, k_vF) < k_INNER_RADIUS) { _onlocal_pocketball(id); return; } if (A.z > k_vF.z) { _onlocal_pocketball(id); return; } if (A.z > -A.x + k_vE.x + k_vE.z) { _onlocal_pocketball(id); return; } } GameObject g_ball_current; // Pocketless table void _phy_ball_table_carom(int id) { float zz, zx; Vector3 A; A = ball_CO[id]; // Setup major regions zx = Mathf.Sign(A.x); zz = Mathf.Sign(A.z); if (A.x * zx > k_pR.x) { ball_CO[id].x = k_pR.x * zx; _phy_bounce_cushion(id, Vector3.left * zx); } if (A.z * zz > k_pO.z) { ball_CO[id].z = k_pO.z * zz; _phy_bounce_cushion(id, Vector3.back * zz); } } void _phy_ball_table_std(int id) { Vector3 A, N, _V, V, a_to_v; float dot; A = ball_CO[id]; _sign_pos.x = Mathf.Sign(A.x); _sign_pos.z = Mathf.Sign(A.z); A = Vector3.Scale(A, _sign_pos); #if HT8B_DRAW_REGIONS Debug.DrawLine( k_vA, k_vB, Color.white ); Debug.DrawLine( k_vD, k_vA, Color.white ); Debug.DrawLine( k_vB, k_vY, Color.white ); Debug.DrawLine( k_vD, k_vX, Color.white ); Debug.DrawLine( k_vC, k_vW, Color.white ); Debug.DrawLine( k_vC, k_vZ, Color.white ); r_k_CUSHION_RADIUS = k_CUSHION_RADIUS-k_BALL_RADIUS; _phy_table_init(); Debug.DrawLine( k_pT, k_pK, Color.yellow ); Debug.DrawLine( k_pK, k_pL, Color.yellow ); Debug.DrawLine( k_pL, k_pM, Color.yellow ); Debug.DrawLine( k_pM, k_pN, Color.yellow ); Debug.DrawLine( k_pN, k_pO, Color.yellow ); Debug.DrawLine( k_pO, k_pP, Color.yellow ); Debug.DrawLine( k_pP, k_pU, Color.yellow ); Debug.DrawLine( k_pV, k_pQ, Color.yellow ); Debug.DrawLine( k_pQ, k_pR, Color.yellow ); Debug.DrawLine( k_pR, k_pS, Color.yellow ); r_k_CUSHION_RADIUS = k_CUSHION_RADIUS; _phy_table_init(); #endif if (A.x > k_vA.x) // Major Regions { if (A.x > A.z + k_MINOR_REGION_CONST) // Minor B { if (A.z < k_TABLE_HEIGHT - k_POCKET_RADIUS) { // Region H #if HT8B_DRAW_REGIONS Debug.DrawLine( new Vector3( 0.0f, 0.0f, 0.0f ), new Vector3( k_TABLE_WIDTH, 0.0f, 0.0f ), Color.red ); Debug.DrawLine( k_vC, k_vC + k_vC_vW_normal, Color.red ); #endif if (A.x > k_TABLE_WIDTH - k_CUSHION_RADIUS) { // Static resolution A.x = k_TABLE_WIDTH - k_CUSHION_RADIUS; // Dynamic _phy_bounce_cushion(id, Vector3.Scale(k_vC_vW_normal, _sign_pos)); } } else { a_to_v = A - k_vC; if (Vector3.Dot(a_to_v, k_vB_vY) > 0.0f) { // Region I ( VORONI ) #if HT8B_DRAW_REGIONS Debug.DrawLine( k_vC, k_pR, Color.green ); Debug.DrawLine( k_vC, k_pQ, Color.green ); #endif if (a_to_v.magnitude < k_CUSHION_RADIUS) { // Static resolution N = a_to_v.normalized; A = k_vC + N * k_CUSHION_RADIUS; // Dynamic _phy_bounce_cushion(id, Vector3.Scale(N, _sign_pos)); } } else { // Region J #if HT8B_DRAW_REGIONS Debug.DrawLine( k_vC, k_vB, Color.red ); Debug.DrawLine( k_pQ, k_pV, Color.blue ); #endif a_to_v = A - k_pQ; if (Vector3.Dot(k_vC_vZ_normal, a_to_v) < 0.0f) { // Static resolution dot = Vector3.Dot(a_to_v, k_vB_vY); A = k_pQ + dot * k_vB_vY; // Dynamic _phy_bounce_cushion(id, Vector3.Scale(k_vC_vZ_normal, _sign_pos)); } } } } else // Minor A { if (A.x < k_vB.x) { // Region A #if HT8B_DRAW_REGIONS Debug.DrawLine( k_vA, k_vA + k_vA_vB_normal, Color.red ); Debug.DrawLine( k_vB, k_vB + k_vA_vB_normal, Color.red ); #endif if (A.z > k_pN.z) { // Velocity based A->C delegation ( scuffed CCD ) a_to_v = A - k_vA; _V = Vector3.Scale(ball_V[id], _sign_pos); V.x = -_V.z; V.y = 0.0f; V.z = _V.x; if (A.z > k_vA.z) { if (Vector3.Dot(V, a_to_v) > 0.0f) { // Region C ( Delegated ) a_to_v = A - k_pL; // Static resolution dot = Vector3.Dot(a_to_v, k_vA_vD); A = k_pL + dot * k_vA_vD; // Dynamic _phy_bounce_cushion(id, Vector3.Scale(k_vA_vD_normal, _sign_pos)); } else { // Static resolution A.z = k_pN.z; // Dynamic _phy_bounce_cushion(id, Vector3.Scale(k_vA_vB_normal, _sign_pos)); } } else { // Static resolution A.z = k_pN.z; // Dynamic _phy_bounce_cushion(id, Vector3.Scale(k_vA_vB_normal, _sign_pos)); } } } else { a_to_v = A - k_vB; if (Vector3.Dot(a_to_v, k_vB_vY) > 0.0f) { // Region F ( VERONI ) #if HT8B_DRAW_REGIONS Debug.DrawLine( k_vB, k_pO, Color.green ); Debug.DrawLine( k_vB, k_pP, Color.green ); #endif if (a_to_v.magnitude < k_CUSHION_RADIUS) { // Static resolution N = a_to_v.normalized; A = k_vB + N * k_CUSHION_RADIUS; // Dynamic _phy_bounce_cushion(id, Vector3.Scale(N, _sign_pos)); } } else { // Region G #if HT8B_DRAW_REGIONS Debug.DrawLine( k_vB, k_vC, Color.red ); Debug.DrawLine( k_pP, k_pU, Color.blue ); #endif a_to_v = A - k_pP; if (Vector3.Dot(k_vB_vY_normal, a_to_v) < 0.0f) { // Static resolution dot = Vector3.Dot(a_to_v, k_vB_vY); A = k_pP + dot * k_vB_vY; // Dynamic _phy_bounce_cushion(id, Vector3.Scale(k_vB_vY_normal, _sign_pos)); } } } } } else { a_to_v = A - k_vA; if (Vector3.Dot(a_to_v, k_vA_vD) > 0.0f) { a_to_v = A - k_vD; if (Vector3.Dot(a_to_v, k_vA_vD) > 0.0f) { if (A.z > k_pK.z) { // Region E #if HT8B_DRAW_REGIONS Debug.DrawLine( k_vD, k_vD + k_vC_vW_normal, Color.red ); #endif if (A.x > k_pK.x) { // Static resolution A.x = k_pK.x; // Dynamic _phy_bounce_cushion(id, Vector3.Scale(k_vC_vW_normal, _sign_pos)); } } else { // Region D ( VORONI ) #if HT8B_DRAW_REGIONS Debug.DrawLine( k_vD, k_vD + k_vC_vW_normal, Color.green ); Debug.DrawLine( k_vD, k_vD + k_vA_vD_normal, Color.green ); #endif if (a_to_v.magnitude < k_CUSHION_RADIUS) { // Static resolution N = a_to_v.normalized; A = k_vD + N * k_CUSHION_RADIUS; // Dynamic _phy_bounce_cushion(id, Vector3.Scale(N, _sign_pos)); } } } else { // Region C #if HT8B_DRAW_REGIONS Debug.DrawLine( k_vA, k_vA + k_vA_vD_normal, Color.red ); Debug.DrawLine( k_vD, k_vD + k_vA_vD_normal, Color.red ); Debug.DrawLine( k_pL, k_pM, Color.blue ); #endif a_to_v = A - k_pL; if (Vector3.Dot(k_vA_vD_normal, a_to_v) < 0.0f) { // Static resolution dot = Vector3.Dot(a_to_v, k_vA_vD); A = k_pL + dot * k_vA_vD; // Dynamic _phy_bounce_cushion(id, Vector3.Scale(k_vA_vD_normal, _sign_pos)); } } } else { // Region B ( VORONI ) #if HT8B_DRAW_REGIONS Debug.DrawLine( k_vA, k_vA + k_vA_vB_normal, Color.green ); Debug.DrawLine( k_vA, k_vA + k_vA_vD_normal, Color.green ); #endif if (a_to_v.magnitude < k_CUSHION_RADIUS) { // Static resolution N = a_to_v.normalized; A = k_vA + N * k_CUSHION_RADIUS; // Dynamic _phy_bounce_cushion(id, Vector3.Scale(N, _sign_pos)); } } } ball_CO[id] = Vector3.Scale(A, _sign_pos); } // Advance simulation 1 step for ball id void _phy_ball_step(int id) { g_ball_current = balls_render[id]; // Since v1.5.0 Vector3 V = ball_V[id]; Vector3 W = ball_W[id]; Vector3 cv; // Equations derived from: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.89.4627&rep=rep1&type=pdf // // R: Contact location with ball and floor aka: (0,-r,0) // µₛ: Slipping friction coefficient // µᵣ: Rolling friction coefficient // i: Up vector aka: (0,1,0) // g: Planet Earth's gravitation acceleration ( 9.80665 ) // // Relative contact velocity (marlow): // c = v + R✕ω // // Ball is classified as 'rolling' or 'slipping'. Rolling is when the relative velocity is none and the ball is // said to be in pure rolling motion // // When ball is classified as rolling: // Δv = -µᵣ∙g∙Δt∙(v/|v|) // // Angular momentum can therefore be derived as: // ωₓ = -vᵤ/R // ωᵧ = 0 // ωᵤ = vₓ/R // // In the slipping state: // Δω = ((-5∙µₛ∙g)/(2/R))∙Δt∙i✕(c/|c|) // Δv = -µₛ∙g∙Δt(c/|c|) // Relative contact velocity of ball and table cv = V + Vector3.Cross(k_CONTACT_POINT, W); // Rolling is achieved when cv's length is approaching 0 // The epsilon is quite high here because of the fairly large timestep we are working with if (cv.magnitude <= 0.1f) { //V += -k_F_ROLL * k_GRAVITY * k_FIXED_TIME_STEP * V.normalized; // (baked): V += -0.00122583125f * V.normalized; // Calculate rolling angular velocity W.x = -V.z * k_BALL_1OR; if (0.3f > Mathf.Abs(W.y)) { W.y = 0.0f; } else { W.y -= Mathf.Sign(W.y) * 0.3f; } W.z = V.x * k_BALL_1OR; // Stopping scenario if (V.magnitude < 0.01f && W.magnitude < 0.2f) { W = Vector3.zero; V = Vector3.zero; } else { ballsMoving = true; } } else // Slipping { Vector3 nv = cv.normalized; // Angular slipping friction //W += ((-5.0f * k_F_SLIDE * k_GRAVITY)/(2.0f * 0.03f)) * k_FIXED_TIME_STEP * Vector3.Cross( Vector3.up, nv ); // (baked): W += -2.04305208f * Vector3.Cross(Vector3.up, nv); //V += -k_F_SLIDE * k_GRAVITY * k_FIXED_TIME_STEP * nv; // (baked): V += -0.024516625f * nv; ballsMoving = true; } ball_W[id] = W; ball_V[id] = V; g_ball_current.transform.Rotate(this.transform.TransformDirection(W.normalized), W.magnitude * k_FIXED_TIME_STEP * -Mathf.Rad2Deg, Space.World); g_ball_current.GetComponent().Play(); uint ball_bit = 0x1U << id; // ball/ball collisions for (int i = id + 1; i < 16; i++) { ball_bit <<= 1; if ((ball_bit & sn_pocketed) != 0U) continue; Vector3 delta = ball_CO[i] - ball_CO[id]; float dist = delta.magnitude; if (dist < k_BALL_DIAMETRE) { Vector3 normal = delta / dist; // static resolution Vector3 res = (k_BALL_DIAMETRE - dist) * normal; ball_CO[i] += res; ball_CO[id] -= res; Vector3 velocityDelta = ball_V[id] - ball_V[i]; float dot = Vector3.Dot(velocityDelta, normal); // Dynamic resolution (Cr is assumed to be (1)+1.0) Vector3 reflection = normal * dot; ball_V[id] -= reflection; ball_V[i] += reflection; // Prevent sound spam if it happens if (ball_V[id].sqrMagnitude > 0 && ball_V[i].sqrMagnitude > 0) { //aud_main.PlayOneShot( snd_Hits[ 0 ], 1.0f ); g_ball_current.GetComponent().PlayOneShot(snd_Hits[id % 3], Mathf.Clamp01(reflection.magnitude)); } // First hit detection if (id == 0) { if (gm_is_2) { if (fb_kr) // KR 사구 ( Sagu ) { if (i == 13) { if (!fb_madefoul) { _onlocal_carompenalize(ball_CO[i]); } } else { if (sn_firsthit == 0) { sn_firsthit = i; } else { if (i != sn_firsthit) { if (sn_secondhit == 0) { sn_secondhit = i; _onlocal_carompoint(ball_CO[i]); } } } } } else // JP 四つ玉 ( Yotsudama ) { if (sn_firsthit == 0) { sn_firsthit = i; } else { if (sn_secondhit == 0) { if (i != sn_firsthit) { sn_secondhit = i; _onlocal_carompoint(ball_CO[i]); } } else { if (sn_thirdhit == 0) { if (i != sn_firsthit && i != sn_secondhit) { sn_thirdhit = i; _onlocal_carompoint(ball_CO[i]); } } } } } } else { if (sn_firsthit == 0) { sn_firsthit = i; } } } } } } // ( Since v0.2.0a ) Check if we can predict a collision before move update happens to improve accuracy bool _phy_predict_cueball() { // Get what will be the next position Vector3 originalDelta = ball_V[0] * k_FIXED_TIME_STEP; Vector3 norm = ball_V[0].normalized; Vector3 h; float lf, s, nmag; // Closest found values float minlf = 9999999.0f; int minid = 0; float mins = 0; uint ball_bit = 0x1U; // Loop balls look for collisions for (int i = 1; i < 16; i++) { ball_bit <<= 1; if ((ball_bit & sn_pocketed) != 0U) continue; h = ball_CO[i] - ball_CO[0]; lf = Vector3.Dot(norm, h); s = k_BALL_DSQRPE - Vector3.Dot(h, h) + lf * lf; if (s < 0.0f) continue; if (lf < minlf) { minlf = lf; minid = i; mins = s; } } if (minid > 0) { nmag = minlf - Mathf.Sqrt(mins); // Assign new position if got appropriate magnitude if (nmag * nmag < originalDelta.sqrMagnitude) { ball_CO[0] += norm * nmag; return true; } } return false; } // Run one physics iteration for all balls void _phys_step() { ballsMoving = false; uint ball_bit = 0x1u; // Cue angular velocity if ((sn_pocketed & 0x1U) == 0) { if (!_phy_predict_cueball()) { // Apply movement ball_CO[0] += ball_V[0] * k_FIXED_TIME_STEP; } _phy_ball_step(0); } // Run main simulation / inter-ball collision for (int i = 1; i < 16; i++) { ball_bit <<= 1; if ((ball_bit & sn_pocketed) == 0U) { ball_CO[i] += ball_V[i] * k_FIXED_TIME_STEP; _phy_ball_step(i); } } // Check if simulation has settled if (!ballsMoving) { if (sn_simulating) { _onlocal_sim_end(); } return; } if (gm_is_2) { _phy_ball_table_carom(0); _phy_ball_table_carom(13); _phy_ball_table_carom(14); _phy_ball_table_carom(15); } else { ball_bit = 0x1U; // Run edge collision for (int i = 0; i < 16; i++) { if ((ball_bit & sn_pocketed) == 0U) _phy_ball_table_std(i); ball_bit <<= 1; } } if (gm_is_2) return; ball_bit = 0x1U; // Run triggers for (int i = 0; i < 16; i++) { if ((ball_bit & sn_pocketed) == 0U) { _phy_ball_pockets(i); } ball_bit <<= 1; } } // Ray circle intersection // yes, its fixed size circle // Output is dispensed into the below variable // One intersection point only // This is not used in physics calcuations, only cue input Vector2 RayCircle_output; bool _phy_ray_circle(Vector2 start, Vector2 dir, Vector2 circle) { Vector2 nrm = dir.normalized; Vector2 h = circle - start; float lf = Vector2.Dot(nrm, h); float s = k_BALL_RSQR - Vector2.Dot(h, h) + lf * lf; if (s < 0.0f) return false; s = Mathf.Sqrt(s); if (lf < s) { if (lf + s >= 0) { s = -s; } else { return false; } } RayCircle_output = start + nrm * (lf - s); return true; } Vector3 RaySphere_output; bool _phy_ray_sphere(Vector3 start, Vector3 dir, Vector3 sphere) { Vector3 nrm = dir.normalized; Vector3 h = sphere - start; float lf = Vector3.Dot(nrm, h); float s = k_BALL_RSQR - Vector3.Dot(h, h) + lf * lf; if (s < 0.0f) return false; s = Mathf.Sqrt(s); if (lf < s) { if (lf + s >= 0) { s = -s; } else { return false; } } RaySphere_output = start + nrm * (lf - s); return true; } // Closest point on line from pos Vector2 _line_project(Vector2 start, Vector2 dir, Vector2 pos) { return start + dir * Vector2.Dot(pos - start, dir); } #endregion #region R_UTIL // Find the lowest numbered ball, that isnt the cue, on the table // This function finds the VISUALLY represented lowest ball, // since 8 has id 1, the search needs to be split int _lowest_ball(uint field) { for (int i = 2; i <= 8; i++) { if (((field >> i) & 0x1U) == 0x00U) return i; } if (((field) & 0x2U) == 0x00U) return 1; for (int i = 9; i < 16; i++) { if (((field >> i) & 0x1U) == 0x00U) return i; } // ?? return 0; } #endregion #region R_TURNRULES void _turn_win(uint winner) { #if HT8B_DEBUGGER _log( LOG_LOW + " -> GAMEOVER" + LOG_END ); #endif sn_gameover = true; sn_winnerid = winner; _netpack(sn_turnid); _netread(); _onlocal_gameover(); } void _turn_pass() { #if HT8B_DEBUGGER _log( LOG_LOW + " -> PASS" + LOG_END ); #endif sn_permit = true; _netpack(sn_turnid ^ 0x1u); _netread(); } void _turn_foul() { #if HT8B_DEBUGGER _log( LOG_LOW + " -> FOUL" + LOG_END ); #endif sn_foul = true; sn_permit = true; _netpack(sn_turnid ^ 0x1U); _netread(); } void _turn_continue() { #if HT8B_DEBUGGER _log( LOG_LOW + " -> COTNINUE" + LOG_END ); #endif // Close table if it was open ( 8 ball specific ) if (gm_is_0) { if (sn_open) { uint sink_orange = 0; uint sink_blue = 0; uint pmask = sn_pocketed >> 2; for (int i = 0; i < 7; i++) { if ((pmask & 0x1u) == 0x1u) sink_blue++; pmask >>= 1; } for (int i = 0; i < 7; i++) { if ((pmask & 0x1u) == 0x1u) sink_orange++; pmask >>= 1; } if (sink_blue == sink_orange) { // Sunk equal amounts therefore still undecided } else { if (sink_blue > sink_orange) { sn_playerxor = sn_turnid; } else { sn_playerxor = sn_turnid ^ 0x1u; } sn_open = false; _onlocal_tableclosed(); } } } // Keep playing sn_permit = true; _netpack(sn_turnid); _netread(); } #endregion #region R_MENU Vector3 m_planenormal; float m_planedist; Vector3 m_cursor; bool m_desktop = true; const float k_mGmButtonW = 0.09345f; const float k_mGmButtonH = 0.034f; const float k_mSmolButtonR = 0.034f; const float k_mGmButtonA = 0.01026977f; // Reset height [Space(10)] [Header("Menu")] [SerializeField] GameObject[] m_gamemode_buttons; [SerializeField] GameObject[] m_join_buttons; //[SerializeField] GameObject m_startbutton; [SerializeField] GameObject[] m_teambuttons; [SerializeField] GameObject[] m_timebuttons; bool[] m_gm_buttonstates = new bool[4]; [SerializeField] Mesh[] m_buttonmeshes; const int k_EButtonMesh_8ball = 0; const int k_EButtonMesh_9ball = 1; const int k_EButtonMesh_4ball = 2; const int k_EButtonMesh_reserved0 = 3; const int k_EButtonMesh_green = 4; const int k_EButtonMesh_red = 5; const int k_EButtonMesh_blue = 6; const int k_EButtonMesh_triangle = 7; const int k_EButtonMesh_join_0 = 8; const int k_EButtonMesh_join_1 = 9; const int k_EButtonMesh_play = 10; const uint k_ButtonState_None = 0x0u; const uint k_ButtonState_Pressing = 0x1u; const uint k_ButtonState_Triggered = 0x2u; const uint k_ButtonState_ShouldReset = 0x2u; const uint k_ButtonState_FrameMask = 0xFFFFFFFEu; // (~0x1u) // Current check dimensions ( pulled from above ) float m_current_x = 0.0f; float m_current_y = 0.0f; Mesh m_current_outline; MeshFilter m_outline_filter; uint[] m_auto_btnstate = new uint[20]; GameObject[] m_auto_btnobjs = new GameObject[20]; int m_auto_id = -1; [SerializeField] GameObject m_gm_dkoutline; [SerializeField] GameObject[] m_playerslot_owners; // VFX stuff [SerializeField] GameObject m_TeamCover; [SerializeField] GameObject m_TimeLimitDisp; Vector3 m_TeamCover_target_s; Vector3 m_TeamCover_current_s; Vector3 m_TimeLimit_x_target; Vector3 m_TimeLimit_x_current; [SerializeField] GameObject m_menuLoc_main; [SerializeField] GameObject m_menuLoc_start; [SerializeField] GameObject m_newGameBtn; [SerializeField] Text[] m_lobbyNames; [SerializeField] GameObject[] rulePages; Vector3 m_menuLoc_sw; Vector3 m_menuLoc_swt; VRCPlayerApi localplayer; Vector3 _plane_line_intersect(Vector3 n, float d, Vector3 a, Vector3 b) { Vector3 ba = b - a; float nDotA = Vector3.Dot(n, a); float nDotBA = Vector3.Dot(n, ba); return a + (((d - nDotA) / nDotBA) * ba); } // Setup meshes on gameobject void _htbtn_init(GameObject button, int variant, bool state) { button.GetComponent().sharedMesh = m_buttonmeshes[variant * 3 + (state ? 1 : 0)]; } void _htmenu_init() { // Setup button meshes _htbtn_init(m_gamemode_buttons[0], k_EButtonMesh_8ball, sn_gamemode == 0u); _htbtn_init(m_gamemode_buttons[1], k_EButtonMesh_9ball, sn_gamemode == 1u); _htbtn_init(m_gamemode_buttons[2], k_EButtonMesh_4ball, sn_gamemode == 2u); _htbtn_init(m_join_buttons[0], k_EButtonMesh_join_0, false); _htbtn_init(m_join_buttons[1], k_EButtonMesh_join_1, false); //_htbtn_init( m_startbutton, k_EButtonMesh_play, false ); _htbtn_init(m_teambuttons[0], k_EButtonMesh_red, !sn_teams); _htbtn_init(m_teambuttons[1], k_EButtonMesh_green, sn_teams); _htbtn_init(m_timebuttons[0], k_EButtonMesh_triangle, true); _htbtn_init(m_timebuttons[1], k_EButtonMesh_triangle, true); _htbtn_init(m_newGameBtn, k_EButtonMesh_play, true); m_outline_filter = m_gm_dkoutline.GetComponent(); // Create surface plane m_planenormal = m_base.transform.up; m_planedist = Vector3.Dot(m_base.transform.position, m_planenormal); localplayer = Networking.LocalPlayer; m_gm_dkoutline.SetActive(false); sn_lobbyclosed = true; _htmenu_viewtimer(); _htmenu_viewteams(); _htmenu_viewgm(); _htmenuview(); } // View gamemode changes void _htmenu_viewgm() { for (int i = 0; i < m_gamemode_buttons.Length; i++) { if (sn_gamemode == (uint)i) { m_gamemode_buttons[i].GetComponent().sharedMesh = m_buttonmeshes[(k_EButtonMesh_8ball + i) * 3 + 1]; } else { m_gamemode_buttons[i].GetComponent().sharedMesh = m_buttonmeshes[(k_EButtonMesh_8ball + i) * 3]; } } } void _htmenu_viewjoin() { int playernum = 0; if (!sn_lobbyclosed) { VRCPlayerApi host = Networking.GetOwner(m_playerslot_owners[0]); // Check out player names for (int i = 0; i < (sn_teams ? 4 : 2); i++) { VRCPlayerApi player = Networking.GetOwner(m_playerslot_owners[i]); // Its us if (local_playerid == i) { // Error: Local believes that we are in lobby, but someone else is there if (player.playerId != Networking.LocalPlayer.playerId) { #if HT8B_DEBUGGER _log( LOG_ERR + "Error: de-sync local lobby status" + LOG_END ); #endif local_playerid = -1; m_lobbyNames[i].text = "CONFLICT"; } else { playernum++; m_lobbyNames[i].text = "" + player.displayName + ""; } } else { // Player is joined if (host.playerId != player.playerId || i == 0) { m_lobbyNames[i].text = "" + player.displayName + ""; playernum++; } else { m_lobbyNames[i].text = ""; } } } } gm_practice = local_playerid == 0 && playernum == 1; // If in the game if (local_playerid >= 0) { // Set our team button to the 'leave' button _htbtn_init(m_join_buttons[local_teamid], k_EButtonMesh_join_0 + (int)local_teamid, true); // Opposite button should become startgame/disabled, 'enabled' if player 0 if (local_playerid == 0) { m_join_buttons[1].SetActive(true); _htbtn_init(m_join_buttons[1], k_EButtonMesh_play, true); } else { m_join_buttons[local_teamid ^ 0x1u].SetActive(false); } } else // Otherwise, its just join buttons { m_join_buttons[0].SetActive(true); m_join_buttons[1].SetActive(true); _htbtn_init(m_join_buttons[0], k_EButtonMesh_join_0, false); _htbtn_init(m_join_buttons[1], k_EButtonMesh_join_1, false); } } uint last_viewtimer = 0u; bool sound_spinning = false; void _htmenu_viewtimer() { if (last_viewtimer != sn_timer) { aud_main.PlayOneShot(snd_spin); last_viewtimer = sn_timer; sound_spinning = true; } m_TimeLimit_x_target = new Vector3(-0.128f * (float)sn_timer, 0.0f, 0.0f); } void _htmenu_viewteams() { m_TeamCover_target_s = sn_teams ? new Vector3(0, 1, 1) : new Vector3(1, 1, 1); _htbtn_init(m_teambuttons[0], k_EButtonMesh_red, !sn_teams); _htbtn_init(m_teambuttons[1], k_EButtonMesh_green, sn_teams); _htmenu_viewjoin(); } void _htmenuview() { if (sn_lobbyclosed) { m_menuLoc_swt = Vector3.one; } else { m_menuLoc_swt = Vector3.zero; } } uint gm_target = 0u; float gm_minheight = Mathf.Infinity; bool _buttonpressed(GameObject btn, int typeid) { // Set automatic id's m_auto_id++; m_auto_btnobjs[m_auto_id] = btn; // Dont process buttons that are disabled if (!btn.activeSelf) { return false; } Vector3 delta; Vector3 tmp_pos; delta = btn.transform.localPosition - m_cursor; if (Mathf.Abs(delta.x) < m_current_x && Mathf.Abs(delta.z) < m_current_y) { if (m_desktop) { // Visual transform if (Input.GetButton("Fire1")) { tmp_pos = btn.transform.localPosition; tmp_pos.y = 0.0f; btn.transform.localPosition = tmp_pos; } m_gm_dkoutline.SetActive(true); m_gm_dkoutline.transform.localPosition = btn.transform.localPosition; m_gm_dkoutline.transform.localRotation = btn.transform.localRotation; m_outline_filter.sharedMesh = m_buttonmeshes[typeid * 3 + 2]; // Actuation if (Input.GetButtonDown("Fire1")) { _htmenu_buttonpressed(); return true; } } else // VR { // Button range if (m_cursor.y < k_mGmButtonA && m_cursor.y > -0.1f) { // Update visual position tmp_pos = btn.transform.localPosition; tmp_pos.y = Mathf.Clamp(m_cursor.y, 0.0f, tmp_pos.y); btn.transform.localPosition = tmp_pos; m_auto_btnstate[m_auto_id] |= k_ButtonState_Pressing; if (m_cursor.y <= 0.0f) // Actuation { // Rising edge if (m_auto_btnstate[m_auto_id] == k_ButtonState_Pressing) { m_auto_btnstate[m_auto_id] |= k_ButtonState_Triggered; _htmenu_buttonpressed(); return true; } } } } } return false; } // Join lobby void _htjoinplayer(int id) { #if HT8B_DEBUGGER _log( LOG_YES + "_htjoinplayer: " + id + LOG_END ); #endif local_playerid = id; local_teamid = ((uint)id & 0x2u) >> 1; Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[id]); _htmenu_viewjoin(); } // Join team locally void _htjointeam(int id) { #if HT8B_DEBUGGER _log( LOG_LOW + "_htjointeam: " + id + LOG_END ); #endif // Leave routine if (local_playerid >= 0) { // Close lobby if (local_playerid == 0) { if (id == 0) { #if HT8B_DEBUGGER _log( LOG_ERR + "( closing lobby )" + LOG_END ); #endif sn_lobbyclosed = true; local_playerid = -1; _htmenuview(); Networking.SetOwner(Networking.LocalPlayer, this.gameObject); _netpack_lossy(); } else { #if HT8B_DEBUGGER _log( LOG_YES + "Starting game!" + LOG_END ); #endif region_selected = false; _tr_newgame(); return; } } else { if ((int)local_teamid == id) { #if HT8B_DEBUGGER _log( LOG_WARN + "( leaving lobby )" + LOG_END ); #endif // Set owner back to host Networking.SetOwner(Networking.GetOwner(m_playerslot_owners[0]), m_playerslot_owners[local_playerid]); // Mark locally out of game local_playerid = -1; } else { #if HT8B_DEBUGGER _log( LOG_LOW + "this button does nothing" + LOG_END ); #endif } } _htmenu_viewjoin(); return; } // Create new lobby if (sn_lobbyclosed) { #if HT8B_DEBUGGER _log( LOG_YES + "Creating lobby" + LOG_END ); #endif // Assign other players to us to signify not joined Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[1]); Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[2]); Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[3]); sn_lobbyclosed = false; Networking.SetOwner(Networking.LocalPlayer, this.gameObject); _netpack_lossy(); _htjoinplayer(0); _htmenuview(); return; } VRCPlayerApi gameHost = Networking.GetOwner(m_playerslot_owners[0]); // Check for open spot on team // Team 1 if (id == 1) { if (Networking.GetOwner(m_playerslot_owners[1]).playerId == gameHost.playerId) { _htjoinplayer(1); } else if (sn_teams && (Networking.GetOwner(m_playerslot_owners[3]).playerId == gameHost.playerId)) { _htjoinplayer(3); } else { #if HT8B_DEBUGGER _log( LOG_ERR + "no slot availible" + LOG_END ); #endif } } // Team 2 else if (sn_teams && (Networking.GetOwner(m_playerslot_owners[2]).playerId == gameHost.playerId)) { _htjoinplayer(2); } else { #if HT8B_DEBUGGER _log( LOG_ERR + "no slot availible" + LOG_END ); #endif } } void _htmenu_resetnetwork() { sn_lobbyclosed = true; if (Networking.GetOwner(this.gameObject) == Networking.LocalPlayer) { Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[0]); Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[1]); Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[2]); Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[3]); _netpack_lossy(); } } // Find button target void _htmenu_trimin() { m_auto_id = -1; GameObject btn; // Join / Leave buttons m_current_x = k_mGmButtonW; m_current_y = k_mGmButtonH; if (sn_lobbyclosed) { if (_buttonpressed(m_newGameBtn, k_EButtonMesh_play)) { _htjointeam(0); } } else { for (int i = 0; i < m_join_buttons.Length; i++) { btn = m_join_buttons[i]; if (_buttonpressed(btn, k_EButtonMesh_join_0 + i)) { _htjointeam(i); } } if (local_playerid == 0) // Host only { // Gamemode buttons m_current_x = k_mGmButtonW; m_current_y = k_mGmButtonH; for (int i = 0; i < m_gamemode_buttons.Length; i++) { btn = m_gamemode_buttons[i]; if (_buttonpressed(btn, k_EButtonMesh_8ball + i)) { sn_gamemode = (uint)i; _htmenu_viewgm(); _netpack_lossy(); } } // Smol buttons m_current_x = k_mSmolButtonR; m_current_y = k_mSmolButtonR; // Timelimit buttons if (_buttonpressed(m_timebuttons[1], k_EButtonMesh_triangle)) { if (sn_timer > 0) { sn_timer--; _htmenu_viewtimer(); _netpack_lossy(); } } if (_buttonpressed(m_timebuttons[0], k_EButtonMesh_triangle)) { if (sn_timer < 2) { sn_timer++; _htmenu_viewtimer(); _netpack_lossy(); } } // Teams enabled buttons if (_buttonpressed(m_teambuttons[0], k_EButtonMesh_red)) { sn_teams = false; // Kick players Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[2]); Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[3]); _htmenu_viewteams(); _netpack_lossy(); } if (_buttonpressed(m_teambuttons[1], k_EButtonMesh_green)) { sn_teams = true; _htmenu_viewteams(); _netpack_lossy(); } } } } VRC_Pickup.PickupHand _htmenu_hand = VRC_Pickup.PickupHand.None; void _htmenu_buttonpressed() { aud_main.PlayOneShot(snd_btn); if (_htmenu_hand != VRC_Pickup.PickupHand.None) { Networking.LocalPlayer.PlayHapticEventInHand(_htmenu_hand, 0.02f, 1.0f, 1.0f); } } void _htmenu_begin() { Vector3 tmp_pos; GameObject btn; for (int i = 0; i <= m_auto_id; i++) { if (m_auto_btnstate[i] == k_ButtonState_ShouldReset) { m_auto_btnstate[i] = k_ButtonState_None; } // Reset button Y position btn = m_auto_btnobjs[i]; tmp_pos = btn.transform.localPosition; tmp_pos.y = k_mGmButtonA; btn.transform.localPosition = tmp_pos; // Disables pressed so it can be re-set m_auto_btnstate[i] &= k_ButtonState_FrameMask; } } void _htmenu_enter() { m_base.SetActive(true); _htmenu_resetnetwork(); _htmenu_viewtimer(); _htmenu_viewteams(); _htmenu_viewgm(); _htmenu_viewjoin(); _htmenuview(); } void _htmenu_exit() { sn_lobbyclosed = true; m_base.SetActive(false); } float next_refresh = 0.0f; void _htmenu_update() { #if UNITY_EDITOR return; #endif m_desktop = !Networking.LocalPlayer.IsUserInVR(); if (Time.timeSinceLevelLoad > next_refresh) { _htmenu_viewjoin(); next_refresh = Time.timeSinceLevelLoad + 0.5f; } // Desktop: Project cursor onto plane if (m_desktop) { m_gm_dkoutline.SetActive(false); VRCPlayerApi.TrackingData hmd = localplayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head); m_cursor = _plane_line_intersect(m_planenormal, m_planedist, hmd.position, hmd.position + (hmd.rotation * Vector3.forward)); // Localize m_cursor m_cursor = m_base.transform.InverseTransformPoint(m_cursor); _htmenu_hand = VRC_Pickup.PickupHand.None; _htmenu_begin(); _htmenu_trimin(); } else { _htmenu_begin(); // Left hand _htmenu_hand = VRC_Pickup.PickupHand.Left; m_cursor = m_base.transform.InverseTransformPoint(localplayer.GetBonePosition(HumanBodyBones.LeftIndexDistal)); _htmenu_trimin(); VRCPlayerApi.TrackingData leftHand = localplayer.GetTrackingData(VRCPlayerApi.TrackingDataType.LeftHand); m_cursor = m_base.transform.InverseTransformPoint(leftHand.position); _htmenu_trimin(); // Right hand _htmenu_hand = VRC_Pickup.PickupHand.Right; m_cursor = m_base.transform.InverseTransformPoint(localplayer.GetBonePosition(HumanBodyBones.RightIndexDistal)); _htmenu_trimin(); VRCPlayerApi.TrackingData rightHand = localplayer.GetTrackingData(VRCPlayerApi.TrackingDataType.RightHand); m_cursor = m_base.transform.InverseTransformPoint(rightHand.position); _htmenu_trimin(); } // Update visual stuff m_TeamCover_current_s = Vector3.Lerp(m_TeamCover_current_s, m_TeamCover_target_s, Time.deltaTime * 5.0f); m_TimeLimit_x_current = Vector3.Lerp(m_TimeLimit_x_current, m_TimeLimit_x_target, Time.deltaTime * 5.0f); m_menuLoc_sw = Vector3.Lerp(m_menuLoc_sw, m_menuLoc_swt, Time.deltaTime * 5.0f); // Stop sound if (sound_spinning && Vector3.Distance(m_TimeLimit_x_current, m_TimeLimit_x_target) < 0.01f) { sound_spinning = false; aud_main.PlayOneShot(snd_spinstop); } m_TeamCover.transform.localScale = m_TeamCover_current_s; m_TimeLimitDisp.transform.localPosition = m_TimeLimit_x_current; // Menu locations scale swap m_menuLoc_start.transform.localScale = m_menuLoc_sw; m_menuLoc_main.transform.localScale = Vector3.one - m_menuLoc_sw; } #endregion float timeLast; float accum; // Copy current values to previous values, no memcpy here >:( void _sn_cpyprev() { // Init _prv states sn_turnid_prv = sn_turnid; sn_open_prv = sn_open; sn_gameover_prv = sn_gameover; sn_gameid_prv = sn_gameid; // Since 1.0.0 sn_gamemode_prv = sn_gamemode; sn_timer_prv = sn_timer; sn_teams_prv = sn_teams; sn_lobbyclosed_prv = sn_lobbyclosed; //sn_pocketed_prv = sn_pocketed; this one needs to be independent //sn_simulating_prv = sn_simulating; //sn_foul_prv = sn_foul; //sn_playerxor_prv = sn_playerxor; //sn_winnerid_prv = sn_winnerid; //sn_permit_prv = sn_permit; } Vector3 dkCursor = new Vector3(0.0f, 2.0f, 0.0f); Vector3 dkHitCursor = new Vector3(0.0f, 0.0f, 0.0f); [Space(10)] [Header("Desktop UI")] [SerializeField] GameObject dkCursorObj; [SerializeField] GameObject dkHitPos; [SerializeField] GameObject desktopBase; [SerializeField] GameObject desktopQuad; [SerializeField] GameObject[] dkStickBases; [SerializeField] GameObject dkOverlayPwr; [SerializeField] GameObject dk_E; const float k_DesktopCursorSpeed = 0.035f; bool dkShootingIn = false; bool dkSafeRemove = true; Vector3 dkShootVector; Vector3 dkSafeRemovePoint; float dkShootReference = 0.0f; float dkClampX; float dkClampY; bool turnLocalLive = false; bool dkFrameIgnore = false; // Cue picked up local public void _ht_desktop_enter() { dk_shootui = true; dkFrameIgnore = true; desktopBase.SetActive(true); // Lock player in place Networking.LocalPlayer.SetWalkSpeed(0.0f); Networking.LocalPlayer.SetRunSpeed(0.0f); Networking.LocalPlayer.SetStrafeSpeed(0.0f); #if HT8B_DEBUGGER _log( LOG_LOW + "Entering desktop overlay" + LOG_END ); #endif } // Cue put down local public void _ht_desktop_cue_down() { _ht_desktopui_exit(); } void _ht_desktopui_exit() { dk_shootui = false; desktopBase.SetActive(false); #if !HT_QUEST gripControllers[0]._primarycontrol(); gripControllers[1]._primarycontrol(); Networking.LocalPlayer.SetWalkSpeed(2.0f); Networking.LocalPlayer.SetRunSpeed(4.0f); Networking.LocalPlayer.SetStrafeSpeed(2.0f); #endif } void _dk_AllowHit() { turnLocalLive = true; // Reset hit point dkHitCursor = Vector3.zero; } void _dk_DenyHit() { turnLocalLive = false; } float shootAmt = 0.0f; void _ht_desktopui_update() { if (dkFrameIgnore) { dkFrameIgnore = false; return; } if (Input.GetKeyDown(KeyCode.E)) { _ht_desktopui_exit(); return; } // Keep UI rendering VRCPlayerApi.TrackingData hmd = localplayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head); desktopQuad.transform.position = hmd.position + hmd.rotation * Vector3.forward; dk_E.transform.position = desktopQuad.transform.position; dkCursor.x = Mathf.Clamp ( dkCursor.x + Input.GetAxis("Mouse X") * k_DesktopCursorSpeed, -dkClampX, dkClampX ); dkCursor.z = Mathf.Clamp ( dkCursor.z + Input.GetAxis("Mouse Y") * k_DesktopCursorSpeed, -dkClampY, dkClampY ); if (turnLocalLive) { Vector3 ncursor = dkCursor; ncursor.y = 0.0f; Vector3 delta = ncursor - ball_CO[0]; GameObject cue = dkStickBases[sn_turnid]; if (Input.GetButton("Fire1")) { if (!dkShootingIn) { dkShootingIn = true; // Create shooting vector dkShootVector = delta.normalized; // Project reference start point dkShootReference = Vector3.Dot(dkShootVector, ncursor); // Create copy of cursor for later dkSafeRemovePoint = dkCursor; // Unlock cursor position from table dkClampX = Mathf.Infinity; dkClampY = Mathf.Infinity; } // Calculate shoot amount via projection shootAmt = dkShootReference - Vector3.Dot(dkShootVector, ncursor); dkSafeRemove = shootAmt < 0.0f; shootAmt = Mathf.Clamp(shootAmt, 0.0f, 0.5f); // Set delta back to dkShootVector delta = dkShootVector; // Disable cursor in shooting mode dkCursorObj.SetActive(false); } else { // Trigger shot if (dkShootingIn) { // Shot cancel if (dkSafeRemove) { } else // FIREEEEE { // Fake hit ( kinda ) float vel = Mathf.Pow(shootAmt * 2.0f, 1.4f) * 9.0f; Vector3 r_1 = (RaySphere_output - ball_CO[0]) * k_BALL_1OR; ball_V[0] = dkShootVector * vel * Vector3.Dot(r_1, -dkShootVector); Vector3 p = dkShootVector.normalized * vel; ball_W[0] = Vector3.Cross(r_1, p) * -25.0f; #if HT8B_DEBUGGER _log( LOG_WARN + "Angular velocity: " + ball_W[ 0 ].ToString() + ". Velocity: " + ball_V[ 0 ].ToString() + LOG_END ); #endif cue.transform.localPosition = new Vector3(2000.0f, 2000.0f, 2000.0f); turnLocalLive = false; _hit_general(); } // Restore cursor position dkCursor = dkSafeRemovePoint; dkClampX = k_TABLE_WIDTH; dkClampY = k_TABLE_HEIGHT; // 1-frame override to fix rotation delta = dkShootVector; } dkShootingIn = false; shootAmt = 0.0f; dkCursorObj.SetActive(true); } if (Input.GetKey(KeyCode.W)) { dkHitCursor += Vector3.forward * Time.deltaTime; } if (Input.GetKey(KeyCode.S)) { dkHitCursor += Vector3.back * Time.deltaTime; } if (Input.GetKey(KeyCode.A)) { dkHitCursor += Vector3.left * Time.deltaTime; } if (Input.GetKey(KeyCode.D)) { dkHitCursor += Vector3.right * Time.deltaTime; } // Clamp in circlee if (dkHitCursor.magnitude > 0.90f) { dkHitCursor = dkHitCursor.normalized * 0.9f; } dkHitPos.transform.localPosition = dkHitCursor; // Get angle float ang = Mathf.Atan2(delta.x, delta.z); // Create rotation Quaternion xr = Quaternion.AngleAxis(10.0f, Vector3.right); Quaternion r = Quaternion.AngleAxis(ang * Mathf.Rad2Deg, Vector3.up); Vector3 worldHit = new Vector3(dkHitCursor.x * k_BALL_PL_X, dkHitCursor.z * k_BALL_PL_X, -0.89f - shootAmt); cue.transform.localRotation = r * xr; cue.transform.position = transform_Surface.TransformPoint(ball_CO[0] + (r * xr * worldHit)); } dkCursorObj.transform.localPosition = dkCursor; dkOverlayPwr.transform.localScale = new Vector3(1.0f - (shootAmt * 2.0f), 1.0f, 1.0f); } #region R_UNITY_MAGIC void _hit_general() { // Make sure repositioner is turned off if the player decides he just // wanted to hit it without putting it somewhere isReposition = false; markerObj.SetActive(false); devhit.SetActive(false); guideline.SetActive(false); // Remove locks _tr_endhit(); sn_permit = false; sn_foul = false; // In case did not drop foul marker #if HT8B_DEBUGGER _log( LOG_LOW + "Commiting changes" + LOG_END ); #endif // Commit changes sn_simulating = true; sn_pocketed_prv = sn_pocketed; // Make sure we definately are the network owner Networking.SetOwner(Networking.LocalPlayer, this.gameObject); _netpack(sn_turnid); _netread(); sn_oursim = true; balls_render[0].GetComponent().PlayOneShot(snd_hitball, 1.0f); } private void Update() { // Physics step accumulator routine float time = Time.timeSinceLevelLoad; float timeDelta = time - timeLast; timeLast = time; if (particle_alive) { _vis_floaty_eval(); } if (dk_shootui) { _ht_desktopui_update(); } // Run sim only if things are moving if (sn_simulating) { accum += timeDelta; if (accum > k_MAX_DELTA) { accum = k_MAX_DELTA; } while (accum >= k_FIXED_TIME_STEP) { _phys_step(); accum -= k_FIXED_TIME_STEP; } } else { // Control is in menu behaviour if (sn_gameover) { _htmenu_update(); return; } } // Update rendering objects positions uint ball_bit = 0x1u; for (int i = 0; i < 16; i++) { if ((ball_bit & sn_pocketed) == 0x0u) { balls_render[i].transform.localPosition = ball_CO[i]; } ball_bit <<= 1; } cue_lpos = transform_Surface.InverseTransformPoint(cuetip.transform.position); Vector3 lpos2 = cue_lpos; // if shot is prepared for next hit if (sn_permit) { bool isContact = false; if (isReposition) { // Clamp position to table / kitchen Vector3 temp = markerObj.transform.localPosition; temp.x = Mathf.Clamp(temp.x, -k_pR.x, repoMaxX); temp.z = Mathf.Clamp(temp.z, -k_pO.z, k_pO.z); temp.y = 0.0f; markerObj.transform.localPosition = temp; markerObj.transform.localRotation = Quaternion.identity; ball_CO[0] = temp; balls_render[0].transform.localPosition = temp; isContact = _cue_contacting(); if (isContact) { markerMaterial.SetColor(uniform_marker_colour, k_markerColourNO); } else { markerMaterial.SetColor(uniform_marker_colour, k_markerColourOK); } } Vector3 cueball_pos = ball_CO[0]; if (sn_armed && !isContact) { float sweep_time_ball = Vector3.Dot(cueball_pos - cue_llpos, cue_vdir); // Check for potential skips due to low frame rate if (sweep_time_ball > 0.0f && sweep_time_ball < (cue_llpos - lpos2).magnitude) { lpos2 = cue_llpos + cue_vdir * sweep_time_ball; } // Hit condition is when cuetip is gone inside ball if ((lpos2 - cueball_pos).sqrMagnitude < k_BALL_RSQR) { Vector3 horizontal_force = lpos2 - cue_llpos; horizontal_force.y = 0.0f; // Compute velocity delta float vel = (horizontal_force.magnitude / Time.deltaTime) * 1.5f; Vector3 r = (RaySphere_output - cueball_pos) * k_BALL_1OR; // Clamp velocity input to 16 m/s ( very strong break speed ) ball_V[0] = cue_shotdir * Mathf.Min(vel, 16.0f) * Vector3.Dot(r, -cue_shotdir); // Angular velocity: L=r(normalized)×p Vector3 p = cue_vdir * vel; ball_W[0] = Vector3.Cross(r, p) * -50.0f; #if HT8B_DEBUGGER _log( LOG_WARN + "Angular velocity: " + ball_W[ 0 ].ToString() + ". Velocity: " + ball_V[ 0 ].ToString() + LOG_END ); #endif _hit_general(); } } else { cue_vdir = this.transform.InverseTransformVector(cuetip.transform.forward);//new Vector2( cuetip.transform.forward.z, -cuetip.transform.forward.x ).normalized; // Get where the cue will strike the ball if (_phy_ray_sphere(lpos2, cue_vdir, cueball_pos)) { guideline.SetActive(true); devhit.SetActive(true); devhit.transform.localPosition = RaySphere_output; cue_shotdir = cue_vdir; cue_shotdir.y = 0.0f; if (dk_shootui) { } else { // Compute deflection in VR mode Vector3 scuffdir = (cueball_pos - RaySphere_output); scuffdir.y = 0.0f; cue_shotdir += scuffdir.normalized * 0.17f; } cue_fdir = Mathf.Atan2(cue_shotdir.z, cue_shotdir.x); // Update the prediction line direction guideline.transform.localPosition = ball_CO[0]; guideline.transform.localEulerAngles = new Vector3(0.0f, -cue_fdir * Mathf.Rad2Deg, 0.0f); } else { devhit.SetActive(false); guideline.SetActive(false); } } } cue_llpos = lpos2; // Table outline colour if (sn_gameover) { // Flashing if we won #if !HT_QUEST tableCurrentColour = tableSrcColour * (Mathf.Sin(Time.timeSinceLevelLoad * 3.0f) * 0.5f + 1.0f); #endif infBaseTransform.transform.localPosition = new Vector3(0.0f, Mathf.Sin(Time.timeSinceLevelLoad) * 0.1f, 0.0f); infBaseTransform.transform.Rotate(Vector3.up, 90.0f * Time.deltaTime); } else { #if !HT_QUEST tableCurrentColour = Color.Lerp(tableCurrentColour, tableSrcColour, Time.deltaTime * 3.0f); #else // Run uniform updates at a slower rate on android (/8) ANDROID_UNIFORM_CLOCK ++; if( ANDROID_UNIFORM_CLOCK >= ANDROID_CLOCK_DIVIDER ) { tableCurrentColour = Color.Lerp( tableCurrentColour, tableSrcColour, Time.deltaTime * 24.0f ); tableMaterial.SetColor( uniform_tablecolour, tableCurrentColour ); ANDROID_UNIFORM_CLOCK = 0x00u; } #endif } float time_percentage; if (timer_running) { float timeleft = timer_end - Time.timeSinceLevelLoad; if (timeleft < 0.0f) { _onlocal_timer_end(); time_percentage = 0.0f; } else { time_percentage = 1.0f - (timeleft * timer_recip); } } else { time_percentage = 0.0f; } #if !HT_QUEST tableMaterial.SetColor(uniform_tablecolour, tableCurrentColour); #endif timerMaterial.SetFloat("_TimeFrac", time_percentage); // Intro animation if (introAminTimer > 0.0f) { introAminTimer -= Time.deltaTime; Vector3 temp; float atime; float aitime; if (introAminTimer < 0.0f) introAminTimer = 0.0f; // Cueball drops late temp = balls_render[0].transform.localPosition; atime = Mathf.Clamp(introAminTimer - 0.33f, 0.0f, 1.0f); aitime = (1.0f - atime); temp.y = Mathf.Abs(Mathf.Cos(atime * 6.29f)) * atime * 0.5f; balls_render[0].transform.localPosition = temp; balls_render[0].transform.localScale = new Vector3(aitime, aitime, aitime); for (int i = 1; i < 16; i++) { temp = balls_render[i].transform.localPosition; atime = Mathf.Clamp(introAminTimer - 0.84f - (float)i * 0.03f, 0.0f, 1.0f); aitime = (1.0f - atime); temp.y = Mathf.Abs(Mathf.Cos(atime * 6.29f)) * atime * 0.5f; balls_render[i].transform.localPosition = temp; balls_render[i].transform.localScale = new Vector3(aitime, aitime, aitime); } } } private void Start() { #if HT8B_DEBUGGER _log( LOG_LOW + "(init) Assigning constants & building collision data" + LOG_END ); #endif // static assignment for config vars repoMaxX = k_TABLE_WIDTH; dkClampX = k_TABLE_WIDTH; dkClampY = k_TABLE_HEIGHT; // Overrides for 4 balls use the last 4 of the 16. (shared 9/4 texture) for (int i = 0; i < 4; i++) { meshes_balls_override[i] = balls_render[12 + i].GetComponent().sharedMesh; } meshes_balls_regular[0] = balls_render[0].GetComponent().sharedMesh; for (int i = 0; i < 3; i++) { meshes_balls_regular[i + 1] = balls_render[13 + i].GetComponent().sharedMesh; } Transform table_base = this.transform.Find("intl.table").Find("table_artwork"); auto_pocketblockers = table_base.Find(".4BALL_FILL").gameObject; auto_rackPosition = table_base.Find(".RACK").gameObject; auto_colliderBaseVFX = table_base.Find("collision.vfx").gameObject; transform_Surface = this.transform.Find("intl.balls"); _phy_table_init(); aud_main = this.GetComponent(); _htmenu_init(); _sn_cpyprev(); // turn off guideline _vis_disableobjects(); #if UNITY_EDITOR _vis_updatecoloursources(); return; #endif #if HT8B_DEBUGGER _log( LOG_LOW + "Starting" + LOG_END ); #endif guidelineMat.SetMatrix("_BaseTransform", this.transform.worldToLocalMatrix); reflection_main.RenderProbe(); } #endregion int[] break_order_8ball = { 9, 2, 10, 11, 1, 3, 4, 12, 5, 13, 14, 6, 15, 7, 8 }; int[] break_order_9ball = { 2, 3, 4, 5, 9, 6, 7, 8, 1 }; int[] break_rows_9ball = { 0, 1, 2, 1, 0 }; // Resets local game state to defined state // TODO: Merge this with NewGame() public void _setup_break() { #if HT8B_DEBUGGER _log( LOG_LOW + "SetupBreak()" + LOG_END ); #endif sn_simulating = false; sn_open = true; sn_gameover = false; sn_playerxor = 0; sn_winnerid = 0; // Cue ball ball_CO[0] = new Vector3(-k_SPOT_POSITION_X, 0.0f, 0.0f); ball_V[0] = Vector3.zero; // Start at spot if (gm_is_1) // 9 ball { sn_pocketed = 0xFC00u; for (int i = 0, k = 0; i < 5; i++) { int rown = break_rows_9ball[i]; for (int j = 0; j <= rown; j++) { ball_CO[break_order_9ball[k++]] = new Vector3 ( k_SPOT_POSITION_X + (float)i * k_BALL_PL_Y + UnityEngine.Random.Range(-k_RANDOMIZE_F, k_RANDOMIZE_F), 0.0f, (float)(-rown + j * 2) * k_BALL_PL_X + UnityEngine.Random.Range(-k_RANDOMIZE_F, k_RANDOMIZE_F) ); ball_V[k] = Vector3.zero; ball_W[k] = Vector3.zero; } } } else if (gm_is_2) // 4 ball { sn_pocketed = 0xFDF2u; ball_CO[0] = new Vector3(-k_SPOT_CAROM_X, 0.0f, 0.0f); ball_CO[13] = new Vector3(k_SPOT_CAROM_X, 0.0f, 0.0f); ball_CO[14] = new Vector3(k_SPOT_POSITION_X, 0.0f, 0.0f); ball_CO[15] = new Vector3(-k_SPOT_POSITION_X, 0.0f, 0.0f); ball_V[0] = Vector3.zero; ball_V[13] = Vector3.zero; ball_V[14] = Vector3.zero; ball_V[15] = Vector3.zero; ball_W[0] = Vector3.zero; ball_W[13] = Vector3.zero; ball_W[14] = Vector3.zero; ball_W[15] = Vector3.zero; } else // Normal 8 ball modes { sn_pocketed = 0x00u; for (int i = 0, k = 0; i < 5; i++) { for (int j = 0; j <= i; j++) { ball_CO[break_order_8ball[k++]] = new Vector3 ( k_SPOT_POSITION_X + (float)i * k_BALL_PL_Y + UnityEngine.Random.Range(-k_RANDOMIZE_F, k_RANDOMIZE_F), 0.0f, (float)(-i + j * 2) * k_BALL_PL_X + UnityEngine.Random.Range(-k_RANDOMIZE_F, k_RANDOMIZE_F) ); ball_V[k] = Vector3.zero; ball_W[k] = Vector3.zero; } } } sn_pocketed_prv = sn_pocketed; } #region R_INTERFACING // Purpose: // Public methods which should are called from other behaviours // Player select 4 ball mode Japanese public void _tr_yotsudama() { fb_jp = true; fb_kr = false; region_selected = true; select4b.SetActive(false); _tr_newgame(); } public void _tr_sagu() { fb_jp = false; fb_kr = true; region_selected = true; select4b.SetActive(false); _tr_newgame(); } // Player stopped holding input trigger public void _tr_endhit() { sn_armed = false; #if !HT_QUEST guidelineMat.SetColor("_Colour", k_aimColour_aim); #endif } // Player is holding input trigger public void _tr_starthit() { // Release hit if cuetip is inside of ball if (Vector3.Distance(cuetip.transform.position, ball_CO[0]) < k_BALL_RADIUS) { // TODO: play a sound here to signify error // TODO: practice mode, allow dragging ball about _tr_endhit(); return; } // lock aim variables bool isOurTurn = ((local_playerid >= 0) && (local_teamid == sn_turnid)) || gm_practice; if (isOurTurn) { sn_armed = true; #if !HT_QUEST guidelineMat.SetColor("_Colour", k_aimColour_locked); #endif } } // Player was moving cueball, place it down public void _tr_placeball() { if (!_cue_contacting()) { isReposition = false; markerObj.SetActive(false); sn_permit = true; sn_foul = false; Networking.SetOwner(Networking.LocalPlayer, this.gameObject); // Save out position to remote clients _netpack(sn_turnid); _netread(); } } // Initialize new match as the host public void _tr_newgame() { // Check if game in progress if (sn_gameover) { #if HT8B_DEBUGGER _log( LOG_YES + "Starting new game" + LOG_END ); #endif // Get gamestate rolling sn_gameid++; sn_permit = true; _onlocal_newgame(); sn_turnid = 0; sn_turnid_prv = 0; _onlocal_turnchange(); // Following is overrides of NewGameLocal, for game STARTER only _setup_break(); _vis_apply_tablecolour(0); Networking.SetOwner(Networking.LocalPlayer, this.gameObject); _netpack(0); _netread(); // Override allow repositioning within kitchen // Local effector isReposition = true; repoMaxX = -k_SPOT_POSITION_X; markerObj.transform.localPosition = ball_CO[0]; markerObj.SetActive(true); if (!region_selected) { if (sn_gamemode == 2u) { select4b.SetActive(true); return; } } } else { // Should not be hit since v1.0.0 #if HT8B_DEBUGGER _log( LOG_ERR + "game in progress" + LOG_END ); #endif } } // Completely reset ht8b state public void _tr_force_end() { // Limit reset to totem owners ownly, this will always be someone in the room // but it may not be obvious to players who has the ownership. So a info text // is added above the reset button telling them who can reset if they dont have it // this is simply to prevent trolls running in and force resetting at random if (Networking.LocalPlayer == Networking.GetOwner(playerTotems[0]) || Networking.LocalPlayer == Networking.GetOwner(playerTotems[1]) || sn_gameover || _name_gen.istrust()) { #if HT8B_DEBUGGER _log( LOG_WARN + "Ending game early" + LOG_END ); #endif sn_gameover = true; sn_permit = false; sn_simulating = false; // For good measure in case other clients trigger an event whilst owner sn_packetid += 2; Networking.SetOwner(Networking.LocalPlayer, this.gameObject); _netpack(sn_turnid); _netread(); _onlocal_gameover(); infReset.text = "Reset"; } else { // TODO: Make this a panel #if HT8B_DEBUGGER _log( LOG_ERR + "Reset is availible to: " + Networking.GetOwner( playerTotems[0] ).displayName + " and " + Networking.GetOwner( playerTotems[1] ).displayName + LOG_END ); #endif infReset.text = "Only:\n" + Networking.GetOwner(playerTotems[0]).displayName + " and " + Networking.GetOwner(playerTotems[1]).displayName + "\ncan reset"; } } #endregion #region R_NETWORK const float I16_MAXf = 32767.0f; void _encode_u16(int pos, ushort v) { net_data[pos] = (byte)(v & 0xff); net_data[pos + 1] = (byte)(((uint)v >> 8) & 0xff); } ushort _decode_u16(int pos) { return (ushort)(net_data[pos] | (((uint)net_data[pos + 1]) << 8)); } // 4 char string from Vector2. Encodes floats in: [ -range, range ] to 0-65535 void _encode_vec3(int pos, Vector3 vec, float range) { _encode_u16(pos, (ushort)((vec.x / range) * I16_MAXf + I16_MAXf)); _encode_u16(pos + 2, (ushort)((vec.z / range) * I16_MAXf + I16_MAXf)); } // 6 char string from Vector3. Encodes floats in: [ -range, range ] to 0-65535 void _encode_vec3_full(int pos, Vector3 vec, float range) { _encode_u16(pos, (ushort)((Mathf.Clamp(vec.x, -range, range) / range) * I16_MAXf + I16_MAXf)); _encode_u16(pos + 2, (ushort)((Mathf.Clamp(vec.y, -range, range) / range) * I16_MAXf + I16_MAXf)); _encode_u16(pos + 4, (ushort)((Mathf.Clamp(vec.z, -range, range) / range) * I16_MAXf + I16_MAXf)); } // Decode 4 chars at index to Vector3 (x,z). Decodes from 0-65535 to [ -range, range ] Vector3 _decode_vec3(int start, float range) { ushort _x = _decode_u16(start); ushort _y = _decode_u16(start + 2); float x = ((_x - I16_MAXf) / I16_MAXf) * range; float y = ((_y - I16_MAXf) / I16_MAXf) * range; return new Vector3(x, 0.0f, y); } // Decode 6 chars at index to Vector3. Decodes from 0-65535 to [ -range, range ] Vector3 _decode_vec3_full(int start, float range) { ushort _x = _decode_u16(start); ushort _y = _decode_u16(start + 2); ushort _z = _decode_u16(start + 4); float x = ((_x - I16_MAXf) / I16_MAXf) * range; float y = ((_y - I16_MAXf) / I16_MAXf) * range; float z = ((_z - I16_MAXf) / I16_MAXf) * range; return new Vector3(x, y, z); } float _decode_f32(int addr, float range) { return ((_decode_u16(addr) - I16_MAXf) / I16_MAXf) * range; } Color _decode_colour(int addr) { ushort _r = _decode_u16(addr); ushort _g = _decode_u16(addr + 2); ushort _b = _decode_u16(addr + 4); ushort _a = _decode_u16(addr + 6); return new Color ( ((_r - I16_MAXf) / I16_MAXf) * 20.0f, ((_g - I16_MAXf) / I16_MAXf) * 20.0f, ((_b - I16_MAXf) / I16_MAXf) * 20.0f, ((_a - I16_MAXf) / I16_MAXf) * 20.0f ); } public void _netpack_lossy() { if (!sn_gameover) { #if HT8B_DEBUGGER _log( LOG_ERR + "Critical error: gameover was false when trying to _netpack_lossy()" + LOG_END ); #endif return; } // Game state uint flags = 0x20u; // bit # // Since v1.0.0 flags |= sn_gamemode << 8; // 8 - 3 bits flags |= sn_timer << 13; // 13 - 2 bits if (sn_teams) flags |= 0x8000u; // 15 - 1 bit if (sn_lobbyclosed) flags |= 0x800u; // 1.5.3: sn_winnerid was being discarded, keep value alive for winner text flags |= sn_winnerid << 6; // 6 _encode_u16(0x4C, (ushort)flags); sn_packetid = (ushort)(sn_packetid + 1u); _encode_u16(0x4E, (ushort)(sn_packetid)); _encode_u16(0x50, sn_gameid); netstr = Convert.ToBase64String(net_data, Base64FormattingOptions.None); } // Encode all data of game state into netstr public void _netpack(uint _turnid) { if (local_playerid < 0) { #if HT8B_DEBUGGER _log( LOG_ERR + "Critical error: local_playerid was less than 0 when trying to NetPack()" + LOG_END ); #endif return; } // Garuntee array size by reallocating.. because c# net_data = new byte[0x52]; for (int i = 0; i < 16; i++) { _encode_vec3(i * 4, ball_CO[i], 2.5f); } // Cue ball velocity & angular velocity last _encode_vec3(0x40, ball_V[0], 50.0f); _encode_vec3_full(0x44, ball_W[0], 500.0f); if (gm_is_2) { // Encode player scores into gmspec sn_gmspec = (ushort)(((uint)fb_scores[0]) & 0x0fu); sn_gmspec |= (ushort)((((uint)fb_scores[1]) & 0x0fu) << 4); if (fb_kr) sn_gmspec |= (ushort)0x100u; // 4 ball specifc ( no pocket info ) _encode_u16(0x4A, sn_gmspec); } else { // Encode pocketed imformation _encode_u16(0x4A, (ushort)(sn_pocketed & 0x0000FFFFu)); } // Game state uint flags = 0x0U; // bit # if (sn_simulating) flags |= 0x1U; // 0 flags |= _turnid << 1; // 1 if (sn_foul) flags |= 0x4U; // 2 if (sn_open) flags |= 0x8U; // 3 flags |= sn_playerxor << 4; // 4 if (sn_gameover) flags |= 0x20u; // 5 flags |= sn_winnerid << 6; // 6 if (sn_permit) flags |= 0x80U; // 7 if (sn_lobbyclosed) flags |= 0x800u; // Since v1.0.0 flags |= sn_gamemode << 8; // 8 - 3 bits flags |= sn_timer << 13; // 13 - 2 bits if (sn_teams) flags |= 0x8000u; // 15 - 1 bit _encode_u16(0x4C, (ushort)flags); // Player ID msb gets added to referee any discrepencies between clients // Higher order players get priority because it will be less common // to play 2v2, so we can save most packet id's for normal 1v1 uint msb_playerid = ((uint)local_playerid & 0x2u) >> 1; _encode_u16(0x4E, (ushort)(sn_packetid + 1u + msb_playerid)); _encode_u16(0x50, sn_gameid); netstr = Convert.ToBase64String(net_data, Base64FormattingOptions.None); #if HT8B_DEBUGGER _log( LOG_LOW + "NetPack()" + LOG_END ); #endif } // Decode networking string // TODO: Clean up this function public void _netread() { // CHECK ERROR =================================================================================================== #if HT8B_DEBUGGER _log( LOG_LOW + "incoming base64: " + netstr + LOG_END ); #endif byte[] in_data = Convert.FromBase64String(netstr); if (in_data.Length < 0x52) { #if HT8B_DEBUGGER _log( LOG_WARN + "Sync string too short for decode, skipping\n" + LOG_END ); #endif return; } net_data = in_data; #if HT8B_DEBUGGER _log( LOG_LOW + _netstr_hex() + LOG_END ); #endif // Throw out updates that are possible errournous ushort nextid = _decode_u16(0x4E); // Reset packetid if a new game has started if (nextid <= sn_packetid) { #if HT8B_DEBUGGER _log( LOG_WARN + "Packet ID was old ( " + nextid + " <= " + sn_packetid + " )" + LOG_END ); #endif return; } sn_packetid = nextid; // MAIN DECODE =================================================================================================== _sn_cpyprev(); // Pocketed information // Ball positions, reset velocity for (int i = 0; i < 16; i++) { ball_V[i] = Vector3.zero; ball_W[i] = Vector3.zero; ball_CO[i] = _decode_vec3(i * 4, 2.5f); } ball_V[0] = _decode_vec3(0x40, 50.0f); ball_W[0] = _decode_vec3_full(0x44, 500.0f); sn_pocketed = _decode_u16(0x4A); uint gamestate = _decode_u16(0x4C); sn_simulating = (gamestate & 0x1U) == 0x1U; sn_turnid = (gamestate & 0x2U) >> 1; sn_foul = (gamestate & 0x4U) == 0x4U; sn_open = (gamestate & 0x8U) == 0x8U; sn_playerxor = (gamestate & 0x10U) >> 4; sn_gameover = (gamestate & 0x20U) == 0x20U; sn_winnerid = (gamestate & 0x40U) >> 6; sn_permit = (gamestate & 0x80U) == 0x80U; sn_lobbyclosed = (gamestate & 0x800u) == 0x800u; // Since v1.0.0 sn_gamemode = (gamestate & 0x700u) >> 8; // 3 bits sn_timer = (gamestate & 0x6000u) >> 13; // 2 bits sn_teams = (gamestate & 0x8000u) == 0x8000u; // sn_gameid = _decode_u16(0x50); // Events ========================================================================================================== if (sn_gameid > sn_gameid_prv && !sn_gameover) { // EV: 1 #if HT8B_DEBUGGER _log( LOG_YES + " .EV: 1 (sn_gameid > sn_gameid_prv) -> NewGame" + LOG_END ); #endif _onlocal_newgame(); } // Check if turn was transferred if (sn_turnid != sn_turnid_prv) { // EV: 2 #if HT8B_DEBUGGER _log( LOG_YES + " .EV: 2 (sn_turnid != sn_turnid_prv) -> NewTurn" + LOG_END ); #endif _onlocal_turnchange(); } // Table switches to closed if (sn_open_prv && !sn_open) { // EV: 3 #if HT8B_DEBUGGER _log( LOG_YES + " .EV: 3 (sn_open_prv && !sn_open) -> DisplaySet" + LOG_END ); #endif _onlocal_tableclosed(); } // Check if game is over if (!sn_gameover_prv && sn_gameover) { // EV: 4 #if HT8B_DEBUGGER _log( LOG_YES + " .EV: 4 (!sn_gameover_prv && sn_gamemover) -> Gameover" + LOG_END ); #endif _onlocal_gameover(); return; } if (sn_gameover) { _htmenu_viewtimer(); _htmenu_viewteams(); _htmenu_viewgm(); _htmenu_viewjoin(); _htmenuview(); return; } // Effects colliders need to be turned off when not simulating // to improve pickups being glitchy if (sn_simulating) { auto_colliderBaseVFX.SetActive(true); } else { auto_colliderBaseVFX.SetActive(false); } if (gm_is_2) { sn_gmspec = _decode_u16(0x4A); fb_scores[0] = (int)(sn_gmspec & 0x0fu); fb_scores[1] = (int)((sn_gmspec & 0xf0u) >> 4); fb_kr = (sn_gmspec & 0x100u) == 0x100u; fb_jp = !fb_kr; sn_pocketed = 0x1FFEu; } // Check this every read // Its basically 'turn start' event if (sn_permit) { bool isOurTurn = ((local_playerid >= 0) && (local_teamid == sn_turnid)) || gm_practice; // Check if teammate placed the positioner if (!sn_foul) { #if HT8B_DEBUGGER _log( LOG_YES + " .EV: 3 (!sn_foul && sn_foul_prv && sn_permit) -> Marker placed" + LOG_END ); #endif isReposition = false; markerObj.SetActive(false); } #if !HT_QUEST if (isOurTurn) { // Update for desktop _dk_AllowHit(); } else { _dk_DenyHit(); } #endif if (gm_is_1) { int target = _lowest_ball(sn_pocketed); marker9ball.SetActive(true); marker9ball.transform.localPosition = ball_CO[target]; } _vis_rackballs(); if (sn_timer > 0 && !timer_running) { _timer_reset(); } } else { marker9ball.SetActive(false); timer_running = false; fb_madepoint = false; fb_madefoul = false; sn_firsthit = 0; sn_secondhit = 0; sn_thirdhit = 0; _vis_hidetimers(); // These dissapeared from v1.0.0 for some reason markerObj.SetActive(false); devhit.SetActive(false); guideline.SetActive(false); } _onlocal_updatescorecard(); } string _netstr_hex() { string str = ""; for (int i = 0; i < net_data.Length; i += 2) { ushort v = _decode_u16(i); str += v.ToString("X4"); } return str; } // Wait for updates to the synced netstr public override void OnDeserialization() { if (!string.Equals(netstr, netstr_prv)) { #if HT8B_DEBUGGER _log( LOG_LOW + "OnDeserialization() :: netstr update" + LOG_END ); #endif netstr_prv = netstr; // Check if local simulation is in progress, the event will fire off later when physics // are settled by the client if (sn_simulating) { #if HT8B_DEBUGGER _log( LOG_WARN + "local simulation is still running, the network update will occur after completion" + LOG_END ); #endif sn_updatelock = true; } else { // We are free to read this update _netread(); } } } #endregion #if !HT_QUEST const int LOG_MAX = 32; int LOG_LEN = 0; int LOG_PTR = 0; string[] LOG_LINES = new string[32]; // Print a line to the debugger void _log(string ln) { Debug.Log("[ht8b] " + ln); LOG_LINES[LOG_PTR++] = "[ht8b] " + ln + "\n"; LOG_LEN++; if (LOG_PTR >= LOG_MAX) { LOG_PTR = 0; } if (LOG_LEN > LOG_MAX) { LOG_LEN = LOG_MAX; } string output = "ht8b 1.6.2"; // Add information about game state: output += Networking.IsOwner(Networking.LocalPlayer, this.gameObject) ? "net( OWNER ) " : "net( RECVR ) "; output += sn_simulating ? "sim( ACTIVE ) " : "sim( PAUSED ) "; VRCPlayerApi currentOwner = Networking.GetOwner(this.gameObject); output += "player( " + (currentOwner != null ? currentOwner.displayName : "[null]") + ":" + sn_turnid + " )"; output += "\n---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n"; // Update display for (int i = 0; i < LOG_LEN; i++) { output += LOG_LINES[(LOG_MAX + LOG_PTR - LOG_LEN + i) % LOG_MAX]; } ltext.text = output; } #endif }