Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ jobs:
-skipPackagePluginValidation \
| xcbeautify

build-visionos:
runs-on: macos-26
steps:
- uses: actions/checkout@v4

- name: Select Xcode 26.3
run: sudo xcode-select -s /Applications/Xcode_26.3.0.app/Contents/Developer

- name: Install tools
run: brew install tuist xcbeautify

- name: Generate project
run: tuist generate --no-open

- name: Build (visionOS)
run: |
xcodebuild build \
-workspace Wammer.xcworkspace \
-scheme Wammer \
-destination 'platform=visionOS Simulator,name=Apple Vision Pro' \
-skipPackagePluginValidation \
| xcbeautify

test:
runs-on: macos-26
needs: build
Expand Down
4 changes: 2 additions & 2 deletions Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ let project = Project(
targets: [
.target(
name: "Wammer",
destinations: [.iPhone, .iPad, .macCatalyst],
destinations: [.iPhone, .iPad, .macCatalyst, .appleVision],
product: .app,
bundleId: "org.ncmud.wammer",
deploymentTargets: .iOS("26.0"),
deploymentTargets: .multiplatform(iOS: "26.0", visionOS: "26.0"),
infoPlist: .extendingDefault(with: [
"LSApplicationCategoryType": .string("public.app-category.games"),
"CFBundleDisplayName": .string("MUDWammer"),
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
[![Build](https://github.com/ncmud/Wammer/actions/workflows/ci.yml/badge.svg)](https://github.com/ncmud/Wammer/actions/workflows/ci.yml)
[![Swift](https://img.shields.io/badge/Swift-6.2-orange)](https://swift.org)
[![Platform](https://img.shields.io/badge/platform-iOS%2026-blue)](https://developer.apple.com/ios/)
[![Platform](https://img.shields.io/badge/platform-visionOS%2026-blue)](https://developer.apple.com/visionos/)
![It's dangerous!](https://img.shields.io/badge/You_are_likely_to_be_eaten_by_a-grue-red.svg)
[![Take this.](https://img.shields.io/badge/get-lamp-yellow.svg)](http://getlamp.com)

This is a fork of [MUDRammer](https://github.com/splinesoft/MUDRammer), a MUD client for iPhone and iPad originally created by [Jonathan Hersh](https://github.com/jhersh). MUDRammer was a fantastic piece of work — a polished, accessible, and thoughtfully designed MUD client that served the community well from its first App Store release in February 2013 through its removal in March 2025.

This fork aims to re-release the app under new branding with proper attribution to Jonathan and the original MUDRammer project. The codebase has been modernized to build with current tools and target iOS 26.
This fork aims to re-release the app under new branding with proper attribution to Jonathan and the original MUDRammer project. The codebase has been modernized to build with current tools and targets iOS 26, Mac Catalyst, and visionOS 26.

## Getting Started

Expand Down
2 changes: 1 addition & 1 deletion Vendored/CocoaAsyncSocket/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import PackageDescription
let package = Package(
name: "CocoaAsyncSocket",
platforms: [.iOS(.v26)],
platforms: [.iOS(.v26), .visionOS(.v26)],
products: [.library(name: "CocoaAsyncSocket", targets: ["CocoaAsyncSocket"])],
targets: [
.target(
Expand Down
2 changes: 1 addition & 1 deletion Vendored/DAKeyboardControl/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import PackageDescription
let package = Package(
name: "DAKeyboardControl",
platforms: [.iOS(.v26)],
platforms: [.iOS(.v26), .visionOS(.v26)],
products: [.library(name: "DAKeyboardControl", targets: ["DAKeyboardControl"])],
targets: [
.target(
Expand Down
24 changes: 23 additions & 1 deletion Vendored/DAKeyboardControl/Sources/DAKeyboardControl.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@
//

#import "DAKeyboardControl.h"

#if TARGET_OS_VISION

// No-op stubs for visionOS (keyboard tracking APIs unavailable)
@implementation UIView (DAKeyboardControl)
- (CGFloat)keyboardTriggerOffset { return 0; }
- (void)setKeyboardTriggerOffset:(CGFloat)offset {}
- (BOOL)keyboardWillRecede { return NO; }
- (void)addKeyboardPanningWithActionHandler:(DAKeyboardDidMoveBlock)b {}
- (void)addKeyboardPanningWithFrameBasedActionHandler:(DAKeyboardDidMoveBlock)a constraintBasedActionHandler:(DAKeyboardDidMoveBlock)b {}
- (void)addKeyboardNonpanningWithActionHandler:(DAKeyboardDidMoveBlock)b {}
- (void)addKeyboardNonpanningWithFrameBasedActionHandler:(DAKeyboardDidMoveBlock)a constraintBasedActionHandler:(DAKeyboardDidMoveBlock)b {}
- (void)removeKeyboardControl {}
- (CGRect)keyboardFrameInView { return CGRectZero; }
- (BOOL)isKeyboardOpened { return NO; }
- (void)hideKeyboard { [self endEditing:YES]; }
@end

#else // !TARGET_OS_VISION

#import <objc/runtime.h>


Expand Down Expand Up @@ -707,4 +727,6 @@ - (BOOL)keyboardWillRecede
return touchLocationInKeyboardWindow.y >= thresholdHeight && velocity.y >= 0;
}

@end
@end

#endif // !TARGET_OS_VISION
2 changes: 1 addition & 1 deletion Vendored/FXForms/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import PackageDescription
let package = Package(
name: "FXForms",
platforms: [.iOS(.v26)],
platforms: [.iOS(.v26), .visionOS(.v26)],
products: [.library(name: "FXForms", targets: ["FXForms"])],
targets: [
.target(
Expand Down
136 changes: 60 additions & 76 deletions Vendored/FXForms/Sources/FXForms.m
Original file line number Diff line number Diff line change
Expand Up @@ -535,21 +535,21 @@
NSString *key = dictionary[FXFormFieldKey];
if (!type)
{
dictionary[FXFormFieldType] = type = FXFormFieldInferType(dictionary);

Check warning on line 538 in Vendored/FXForms/Sources/FXForms.m

View workflow job for this annotation

GitHub Actions / analyze

Although the value stored to 'type' is used in the enclosing expression, the value is never actually read from 'type' [deadcode.DeadStores]
}

//convert cell from string to class
id cellClass = dictionary[FXFormFieldCell];
if ([cellClass isKindOfClass:[NSString class]])
{
dictionary[FXFormFieldCell] = cellClass = FXFormClassFromString(cellClass);

Check warning on line 545 in Vendored/FXForms/Sources/FXForms.m

View workflow job for this annotation

GitHub Actions / analyze

Although the value stored to 'cellClass' is used in the enclosing expression, the value is never actually read from 'cellClass' [deadcode.DeadStores]
}

//convert view controller from string to class
id viewController = dictionary[FXFormFieldViewController];
if ([viewController isKindOfClass:[NSString class]])
{
dictionary[FXFormFieldViewController] = viewController = FXFormClassFromString(viewController);

Check warning on line 552 in Vendored/FXForms/Sources/FXForms.m

View workflow job for this annotation

GitHub Actions / analyze

Although the value stored to 'viewController' is used in the enclosing expression, the value is never actually read from 'viewController' [deadcode.DeadStores]
}

//convert header from string to class
Expand Down Expand Up @@ -1102,7 +1102,9 @@
segue = FXFormClassFromString(segue) ?: [segue copy];
}

#if !TARGET_OS_VISION
NSAssert(segue != [UIStoryboardPopoverSegue class], @"Unfortunately displaying subcontrollers using UIStoryboardPopoverSegue is not supported, as doing so would require calling private methods. To display using a popover, create a custom UIStoryboard subclass instead.");
#endif

_segue = segue;
}
Expand Down Expand Up @@ -2377,43 +2379,35 @@
self.originalTableContentInset = tableContentInset;
tableContentInset.bottom = heightOfTableViewThatIsCoveredByKeyboard;

UIEdgeInsets tableScrollIndicatorInsets = self.tableView.scrollIndicatorInsets;
UIEdgeInsets tableScrollIndicatorInsets = self.tableView.verticalScrollIndicatorInsets;
tableScrollIndicatorInsets.bottom += heightOfTableViewThatIsCoveredByKeyboard;

[UIView beginAnimations:nil context:nil];

// adjust the tableview insets by however much the keyboard is overlapping the tableview
self.tableView.contentInset = tableContentInset;
self.tableView.scrollIndicatorInsets = tableScrollIndicatorInsets;

UIView *firstResponder = FXFormsFirstResponder(self.tableView);
if ([firstResponder isKindOfClass:[UITextView class]]) {
UITextView *textView = (UITextView *)firstResponder;

// calculate the position of the cursor in the textView
NSRange range = textView.selectedRange;
UITextPosition *beginning = textView.beginningOfDocument;
UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
UITextPosition *end = [textView positionFromPosition:start offset:range.length];
CGRect caretFrame = [textView caretRectForPosition:end];

// convert the cursor to the same coordinate system as the tableview
CGRect caretViewFrame = [textView convertRect:caretFrame toView:self.tableView.superview];

// padding makes sure that the cursor isn't sitting just above the keyboard and will adjust to 3 lines of text worth above keyboard
CGFloat padding = textView.font.lineHeight * 3;
CGFloat keyboardToCursorDifference = (caretViewFrame.origin.y + caretViewFrame.size.height) - heightOfTableViewThatIsNotCoveredByKeyboard + padding;

// if there is a difference then we want to adjust the keyboard, otherwise the cursor is fine to stay where it is and the keyboard doesn't need to move
if (keyboardToCursorDifference > 0.0f) {
// adjust offset by this difference
CGPoint contentOffset = self.tableView.contentOffset;
contentOffset.y += keyboardToCursorDifference;
[self.tableView setContentOffset:contentOffset animated:YES];

[UIView animateWithDuration:0.25 animations:^{
// adjust the tableview insets by however much the keyboard is overlapping the tableview
self.tableView.contentInset = tableContentInset;
self.tableView.verticalScrollIndicatorInsets = tableScrollIndicatorInsets;
} completion:^(BOOL finished) {
UIView *firstResponder = FXFormsFirstResponder(self.tableView);
if ([firstResponder isKindOfClass:[UITextView class]]) {
UITextView *textView = (UITextView *)firstResponder;

NSRange range = textView.selectedRange;
UITextPosition *beginning = textView.beginningOfDocument;
UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
UITextPosition *end = [textView positionFromPosition:start offset:range.length];
CGRect caretFrame = [textView caretRectForPosition:end];
CGRect caretViewFrame = [textView convertRect:caretFrame toView:self.tableView.superview];

CGFloat padding = textView.font.lineHeight * 3;
CGFloat keyboardToCursorDifference = (caretViewFrame.origin.y + caretViewFrame.size.height) - heightOfTableViewThatIsNotCoveredByKeyboard + padding;

if (keyboardToCursorDifference > 0.0f) {
CGPoint contentOffset = self.tableView.contentOffset;
contentOffset.y += keyboardToCursorDifference;
[self.tableView setContentOffset:contentOffset animated:YES];
}
}
}

[UIView commitAnimations];
}];
}

- (void)keyboardWillHide:(NSNotification *)note
Expand All @@ -2422,17 +2416,15 @@
if (cell && ![self.delegate isKindOfClass:[UITableViewController class]])
{
NSDictionary *keyboardInfo = [note userInfo];
UIEdgeInsets tableScrollIndicatorInsets = self.tableView.scrollIndicatorInsets;
tableScrollIndicatorInsets.bottom = 0;

//restore insets
[UIView beginAnimations:nil context:nil];
[UIView setAnimationCurve:(UIViewAnimationCurve)keyboardInfo[UIKeyboardAnimationCurveUserInfoKey]];
[UIView setAnimationDuration:[keyboardInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
self.tableView.contentInset = self.originalTableContentInset;
self.tableView.scrollIndicatorInsets = tableScrollIndicatorInsets;
self.originalTableContentInset = UIEdgeInsetsZero;
[UIView commitAnimations];
NSTimeInterval duration = [keyboardInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];

[UIView animateWithDuration:duration animations:^{
self.tableView.contentInset = self.originalTableContentInset;
UIEdgeInsets indicatorInsets = self.tableView.verticalScrollIndicatorInsets;
indicatorInsets.bottom = 0;
self.tableView.verticalScrollIndicatorInsets = indicatorInsets;
self.originalTableContentInset = UIEdgeInsetsZero;
}];
}
}

Expand Down Expand Up @@ -2502,7 +2494,7 @@

if (!self.tableView)
{
self.tableView = [[UITableView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds
style:UITableViewStyleGrouped];
}
if (!self.tableView.superview)
Expand Down Expand Up @@ -3019,7 +3011,7 @@
- (BOOL)resignFirstResponder
{
return [self.textField resignFirstResponder];
}

Check warning on line 3014 in Vendored/FXForms/Sources/FXForms.m

View workflow job for this annotation

GitHub Actions / analyze

The 'resignFirstResponder' instance method in UIResponder subclass 'FXFormTextFieldCell' is missing a [super resignFirstResponder] call [osx.cocoa.MissingSuperCall]

@end

Expand Down Expand Up @@ -3208,7 +3200,7 @@
- (BOOL)resignFirstResponder
{
return [self.textView resignFirstResponder];
}

Check warning on line 3203 in Vendored/FXForms/Sources/FXForms.m

View workflow job for this annotation

GitHub Actions / analyze

The 'resignFirstResponder' instance method in UIResponder subclass 'FXFormTextViewCell' is missing a [super resignFirstResponder] call [osx.cocoa.MissingSuperCall]

@end

Expand Down Expand Up @@ -3401,7 +3393,7 @@
@end


@interface FXFormImagePickerCell () <UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIActionSheetDelegate>
@interface FXFormImagePickerCell () <UINavigationControllerDelegate, UIImagePickerControllerDelegate>

@property (nonatomic, strong) UIImagePickerController *imagePickerController;
@property (nonatomic, weak) UIViewController *controller;
Expand Down Expand Up @@ -3484,34 +3476,34 @@
[FXFormsFirstResponder(tableView) resignFirstResponder];
[tableView deselectRowAtIndexPath:tableView.indexPathForSelectedRow animated:YES];

if (!TARGET_IPHONE_SIMULATOR && ![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])
BOOL hasCamera = NO;
#if !TARGET_OS_VISION
hasCamera = !TARGET_IPHONE_SIMULATOR && [UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera];
#endif

if (!hasCamera)
{
self.imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[controller presentViewController:self.imagePickerController animated:YES completion:nil];
}
else if ([UIAlertController class])
else
{
UIAlertControllerStyle style = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)? UIAlertControllerStyleAlert: UIAlertControllerStyleActionSheet;
UIAlertControllerStyle style = ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad)? UIAlertControllerStyleAlert: UIAlertControllerStyleActionSheet;
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:style];

[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Take Photo", nil) style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) {
[self actionSheet:nil didDismissWithButtonIndex:0];
[self showPickerWithSourceType:0];
}]];

[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Photo Library", nil) style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) {
[self actionSheet:nil didDismissWithButtonIndex:1];
[self showPickerWithSourceType:1];
}]];

[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:NULL]];

self.controller = controller;
[controller presentViewController:alert animated:YES completion:NULL];
}
else
{
self.controller = controller;
[[[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:NSLocalizedString(@"Cancel", nil) destructiveButtonTitle:nil otherButtonTitles:NSLocalizedString(@"Take Photo", nil), NSLocalizedString(@"Photo Library", nil), nil] showInView:controller.view];
}
}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
Expand All @@ -3527,23 +3519,15 @@
[self update];
}

- (void)actionSheet:(__unused UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex
- (void)showPickerWithSourceType:(NSInteger)buttonIndex
{
UIImagePickerControllerSourceType sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
switch (buttonIndex)
{
case 0:
{
sourceType = UIImagePickerControllerSourceTypeCamera;
break;
}
case 1:
{
sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
break;
}
#if !TARGET_OS_VISION
if (buttonIndex == 0) {
sourceType = UIImagePickerControllerSourceTypeCamera;
}

#endif

if ([UIImagePickerController isSourceTypeAvailable:sourceType])
{
self.imagePickerController.sourceType = sourceType;
Expand Down
2 changes: 1 addition & 1 deletion Vendored/JSQSystemSoundPlayer/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import PackageDescription
let package = Package(
name: "JSQSystemSoundPlayer",
platforms: [.iOS(.v26)],
platforms: [.iOS(.v26), .visionOS(.v26)],
products: [.library(name: "JSQSystemSoundPlayer", targets: ["JSQSystemSoundPlayer"])],
targets: [
.target(
Expand Down
2 changes: 1 addition & 1 deletion Vendored/JTSImageViewController/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import PackageDescription
let package = Package(
name: "JTSImageViewController",
platforms: [.iOS(.v26)],
platforms: [.iOS(.v26), .visionOS(.v26)],
products: [.library(name: "JTSImageViewController", targets: ["JTSImageViewController"])],
targets: [
.target(
Expand Down
Loading
Loading